6

It just occurred to me that operator+ & co can operate on this for rvalues; i.e. given a class C, it's possible to do this:

class C {
    // ...
    C operator-( const C& rhs ) const & {
        C result = *this;
        result -= rhs;
        return result;
    }
    C&& operator-( const C& rhs ) && {
        *this -= rhs;
        return std::move( *this );
    }
};

This would prevent copies by simply modifying temporary values in-place.

Would this perform as I expect? Is it a reasonable optimization or would the compiler create equally fast code?

Mr. Wonko
  • 690
  • 9
  • 17
  • With the first version you can already get copy ellision. So I'm not sure the second one is needed (if it works at all, I feel like it would bite you in the ass but I could be wrong) – Borgleader Sep 03 '15 at 17:08
  • Yeah, I have a feeling that copy elision might lead to similar performance... But I don't see how my version could bite me in the ass quite yet. (I'm not sure though, hence the question.) – Mr. Wonko Sep 03 '15 at 17:18
  • Object lifetime issues. `C&& c = C() - some_C;` and `c` is dangling. – T.C. Sep 03 '15 at 17:28
  • You appear to be right. I don't quite understand why yet. – Mr. Wonko Sep 03 '15 at 17:34
  • @T.C. You can declare objects to be of rvalue-type? Why? Is that useful for anything? (E.g. why not just `C c = .... ; use_ctype(std::move(c));` ?) – Kyle Strand Sep 03 '15 at 18:31
  • 1
    @KyleStrand Suspend the temporary in mid-air and extend its lifetime if it's of a non-movable type. You rarely need this explicitly, but the range-based for loop does it internally. – T.C. Sep 03 '15 at 18:38
  • Huh. Okay, so when you say that `c` is dangling, is that just because it's now a "moved-from" value? Other than that, I don't quite see why the code will be problematic.... – Kyle Strand Sep 03 '15 at 18:58

1 Answers1

5

Let's say we just wrap std::string and do a simplified version of operator+:

struct C { 
    std::string val;

    C&& operator+(const C& rhs) && {
        val += rhs.val;
        return std::move(*this);
    }   

    std::string::iterator begin() { return val.begin(); }
    std::string::iterator end() { return val.end(); }
};

With that, this works fine:

for (char c : C{"hello"}) { .. }

the range-for expression will extend the lifetime of the temporary, so we're ok. However, consider this:

for (char c : C{"hello"} + C{"goodbye"}) { .. }

We effectively have:

auto&& __range = C{"hello"}.operator+(C{"goodbye"});

Here, we're not binding a temporary to a reference. We're binding a reference. The object doesn't get its lifetime extended because... it's not an object. So we have a dangling reference, and undefined behavior. This would be very surprising to users who would expect this to work:

for (char c : std::string{"hello"} + std::string{"goodbye"}) { .. }

You'd have to return a value:

C operator+(const C& rhs) && {
    val += rhs.val;
    return std::move(*this);
}

That solves this issue (as now we have temporary extension), and if moving your objects is cheaper than copying them, this is a win.

Barry
  • 286,269
  • 29
  • 621
  • 977