Proper way to facilitate MOVE operation when overriding operator on C++

Viewed 146

I'm not really familiar with how move works in C++, and I need some help to clarify my understanding. I'm want to overload the operator+, and I'm have a couple of questions about it.


ap_n operator+(const ap_n& n, const ap_n& m) {
    ap_n tmp {n};
    return tmp += m;
}

My first question is how to make temporary objects movable. As shown in my function above, both arguments are not meant to be mutated, therefore I need to create a third object to do the operation upon.

How can I make my return value usable for move operation. Is the return value supposed to be a reference as ap_n&? Should the return object be encapsulated by std::move(tmp)? Or is it alright as is?

How does C++ decide when an object is rvalue, or decide that a move operation is suitable on an object, and how can I tell the program that an object is safe to use for move operation.


ap_n operator+(const ap_n&, const ap_n&); // defined as in first question
ap_n operator+(ap_n&& n, const ap_n& m) { return n += m; }
ap_n operator+(const ap_n& n, ap_n&& m) { return m += n; }
ap_n operator+(ap_n&& n, ap_n&& m) { return n += m; }

My second question is whether it is necessary to create variants of function that accept rvalue arguments. Right now I have 4 functions, as shown, to be able to accept normal objects and rvalue objects.

Is writing all the combinations possible like this necessary? If I remove all but the first function, would the program still be able to perform move operation correctly?

3 Answers

As a debugging tip, something that can help with getting these things right is to print a message in the move constructor

ap_n(ap_n&& o): x_(std::move(o.x_)) { std::cerr << "Move constructed\n"; }

plus similar messages in other constructors and the destructor. Then you get a clear chronology of when and how instances are created and destroyed.

How can I make my return value usable for move operation. Is the return value supposed to be a reference as ap_n&? Should the return object be encapsulated by std::move(tmp)? Or is it alright as is?

Return the result by value. (Don't return a reference to a local variable, since the local variable goes out of scope immediately, making the reference invalid.) You might find this short article useful: Tip of the Week #77: Temporaries, Moves, and Copies. For more depth, check out cppreference on copy elision.

how to make temporary objects movable

By defining move constructor/assignment to your type.

Is the return value supposed to be a reference as ap_n&?

Not for operator+ when you return new object.

operator += on the other hand returns reference to lhs, so returns ap_n&.

How can I make my return value usable for move operation. Should the return object be encapsulated by std::move(tmp)? Or is it alright as is?

From return statement, there is an automatic move when returning directly a local variable.

so return tmp; is sufficient.

return std::move(tmp); prevents NRVO

return tmp += m; does a copy, as you don't return "directly" tmp.

You should do:

ap_n operator+(const ap_n& n, const ap_n& m) {
    ap_n tmp {n};
    tmp += m;
    return tmp; // NRVO, or automatic move
}

return std::move(tmp += m); would prevent NRVO, and do the move.

How does C++ decide when an object is rvalue,

Roughly,

  • variables are l-value as there have name.
  • function returning l-value reference (ap_n&) returns l-value.
  • function returning r-value reference (ap_n&&), or by value (ap_n) returns r-value.

or decide that a move operation is suitable on an object, and how can I tell the program that an object is safe to use for move operation.

Overload resolution select the best match between valid candidate.

So it requires function taking by value or by r-value reference (or forwarding reference).

My second question is

It seems not the second ;-)

whether it is necessary to create variants of function that accept rvalue arguments. Right now I have 4 functions, as shown, to be able to accept normal objects and rvalue objects.

Is writing all the combinations possible like this necessary?

Single function taking by const reference or by value can be enough in general case, unless you want that optimization. so mostly for library writer, or critical code.

Notice that your overloads should be rewritten to effectively do move operation (to reuse input temporary parameter):

ap_n operator+(ap_n&& n, const ap_n& m)
{
    n += m;
    return std::move(n); // C++11; C++14, C++17
    return n; // C++20
}

or

ap_n&& operator+(ap_n&& n, const ap_n& m)
{
    return std::move(n += m);
}

If I remove all but the first function, would the program still be able to perform move operation correctly?

With

ap_n operator+(const ap_n& n, const ap_n& m) {
    ap_n tmp {n}; // copy
    tmp += m;
    return tmp; // NRVO, or automatic move
}

You have 1 copy, one NRVO/move for any kind of parameters.

With

ap_n&& operator+(ap_n&& n, const ap_n& m) {
    return std::move(n += m);
}

you have no moves, but you should be careful about lifetime of references, as

auto ok = ap_n() + ap_n(); // 1 extra move
auto&& dangling = ap_n() + ap_n(); // No move, but no lifetime extension...

With

ap_n operator+(ap_n&& n, const ap_n& m) {
    n += m;
    return std::move(n); // C++11, C++14, C++17 // move
    return n; // C++20 // automatic move
}

you have 1 move, no copies.

auto ok = ap_n() + ap_n(); // 1 extra move possibly elided pre-C++17, 0 extra moves since C++17
auto&& ok2 = ap_n() + ap_n(); // No moves, and lifetime extension...

So with extra overload, you might trade copy to move.

Taking notes from Jarod42's answer, the following is the revised code.

ap_n operator+(const ap_n& n, const ap_n& m) {
    ap_n tmp {n};
    tmp += n;
    return tmp; // Allows copy, NRVO, or move
}
ap_n operator+(ap_n&& n, const ap_n& m) {
    n += m;
    return std::move(n); // Allows copy, or move
}
ap_n operator+(const ap_n& n, ap_n&& m) {
    m += n;
    return std::move(m); // Allows copy, or move
}

The amount of functions is also reduced to 3, since one that takes both rvalue reference will automatically use the second function.

Please tell me if I'm still misunderstanding this.

Related