Asked  6 Months ago    Answers:  5   Viewed   28 times

I'm trying to understand rvalue references and move semantics of C++11.

What is the difference between these examples, and which of them is going to do no vector copy?

First example

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Second example

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Third example

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

 Answers

79

First example

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

The first example returns a temporary which is caught by rval_ref. That temporary will have its life extended beyond the rval_ref definition and you can use it as if you had caught it by value. This is very similar to the following:

const std::vector<int>& rval_ref = return_vector();

except that in my rewrite you obviously can't use rval_ref in a non-const manner.

Second example

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

In the second example you have created a run time error. rval_ref now holds a reference to the destructed tmp inside the function. With any luck, this code would immediately crash.

Third example

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Your third example is roughly equivalent to your first. The std::move on tmp is unnecessary and can actually be a performance pessimization as it will inhibit return value optimization.

The best way to code what you're doing is:

Best practice

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

I.e. just as you would in C++03. tmp is implicitly treated as an rvalue in the return statement. It will either be returned via return-value-optimization (no copy, no move), or if the compiler decides it can not perform RVO, then it will use vector's move constructor to do the return. Only if RVO is not performed, and if the returned type did not have a move constructor would the copy constructor be used for the return.

Tuesday, June 1, 2021
 
Easen
answered 6 Months ago
85

You've missed a significant optimization in your copy assignment operator. And subsequently the situation has gotten confused.

  AnObject& operator = ( const AnObject& rh )
  {
    if (this != &rh)
    {
      if (n != rh.n)
      {
         delete [] a;
         n = 0;
         a = new int [ rh.n ];
         n = rh.n;
      }
      std::copy(rh.a, rh.a+n, a);
    }
    return *this;
  }

Unless you really never think you'll be assigning AnObjects of the same size, this is much better. Never throw away resources if you can recycle them.

Some might complain that the AnObject's copy assignment operator now has only basic exception safety instead of strong exception safety. However consider this:

Your clients can always take a fast assignment operator and give it strong exception safety. But they can't take a slow assignment operator and make it faster.

template <class T>
T&
strong_assign(T& x, T y)
{
    swap(x, y);
    return x;
}

Your move constructor is fine, but your move assignment operator has a memory leak. It should be:

  AnObject& operator = ( AnObject&& rh )
  {
    delete [] a;
    n = rh.n;
    a = rh.a;
    rh.n = 0;
    rh.a = nullptr;
    return *this;
  }

...

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: Using a combination of copy elision / RVO / move semantics the compiler should be able to this this with a minimum of copying, no?

You may need to overload your operators to take advantage of resources in rvalues:

Data operator+(Data&& x, const Data& y)
{
   // recycle resources in x!
   x += y;
   return std::move(x);
}

Ultimately resources ought to be created exactly once for each Data you care about. There should be no needless new/delete just for the purpose of moving things around.

Thursday, June 3, 2021
 
Tucker
answered 6 Months ago
69

It is correct that std::move(x) is just a cast to rvalue - more specifically to an xvalue, as opposed to a prvalue. And it is also true that having a cast named move sometimes confuses people. However the intent of this naming is not to confuse, but rather to make your code more readable.

The history of move dates back to the original move proposal in 2002. This paper first introduces the rvalue reference, and then shows how to write a more efficient std::swap:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

One has to recall that at this point in history, the only thing that "&&" could possibly mean was logical and. No one was familiar with rvalue references, nor of the implications of casting an lvalue to an rvalue (while not making a copy as static_cast<T>(t) would do). So readers of this code would naturally think:

I know how swap is supposed to work (copy to temporary and then exchange the values), but what is the purpose of those ugly casts?!

Note also that swap is really just a stand-in for all kinds of permutation-modifying algorithms. This discussion is much, much bigger than swap.

Then the proposal introduces syntax sugar which replaces the static_cast<T&&> with something more readable that conveys not the precise what, but rather the why:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

I.e. move is just syntax sugar for static_cast<T&&>, and now the code is quite suggestive as to why those casts are there: to enable move semantics!

One must understand that in the context of history, few people at this point really understood the intimate connection between rvalues and move semantics (though the paper tries to explain that as well):

Move semantics will automatically come into play when given rvalue arguments. This is perfectly safe because moving resources from an rvalue can not be noticed by the rest of the program (nobody else has a reference to the rvalue in order to detect a difference).

If at the time swap was instead presented like this:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(cast_to_rvalue(a));
    a = cast_to_rvalue(b);
    b = cast_to_rvalue(tmp);
}

Then people would have looked at that and said:

But why are you casting to rvalue?


The main point:

As it was, using move, no one ever asked:

But why are you moving?


As the years went on and the proposal was refined, the notions of lvalue and rvalue were refined into the value categories we have today:

Taxonomy

(image shamelessly stolen from dirkgently)

And so today, if we wanted swap to precisely say what it is doing, instead of why, it should look more like:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(set_value_category_to_xvalue(a));
    a = set_value_category_to_xvalue(b);
    b = set_value_category_to_xvalue(tmp);
}

And the question everyone should be asking themselves is if the above code is more or less readable than:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

Or even the original:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

In any event, the journeyman C++ programmer should know that under the hood of move, nothing more is going on than a cast. And the beginner C++ programmer, at least with move, will be informed that the intent is to move from the rhs, as opposed to copy from the rhs, even if they don't understand exactly how that is accomplished.

Additionally, if a programmer desires this functionality under another name, std::move possesses no monopoly on this functionality, and there is no non-portable language magic involved in its implementation. For example if one wanted to code set_value_category_to_xvalue, and use that instead, it is trivial to do so:

template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

In C++14 it gets even more concise:

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

So if you are so inclined, decorate your static_cast<T&&> however you think best, and perhaps you will end up developing a new best practice (C++ is constantly evolving).

So what does move do in terms of generated object code?

Consider this test:

void
test(int& i, int& j)
{
    i = j;
}

Compiled with clang++ -std=c++14 test.cpp -O3 -S, this produces this object code:

__Z4testRiS_:                           ## @_Z4testRiS_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movl    (%rsi), %eax
    movl    %eax, (%rdi)
    popq    %rbp
    retq
    .cfi_endproc

Now if the test is changed to:

void
test(int& i, int& j)
{
    i = std::move(j);
}

There is absolutely no change at all in the object code. One can generalize this result to: For trivially movable objects, std::move has no impact.

Now lets look at this example:

struct X
{
    X& operator=(const X&);
};

void
test(X& i, X& j)
{
    i = j;
}

This generates:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSERKS_           ## TAILCALL
    .cfi_endproc

If you run __ZN1XaSERKS_ through c++filt it produces: X::operator=(X const&). No surprise here. Now if the test is changed to:

void
test(X& i, X& j)
{
    i = std::move(j);
}

Then there is still no change whatsoever in the generated object code. std::move has done nothing but cast j to an rvalue, and then that rvalue X binds to the copy assignment operator of X.

Now lets add a move assignment operator to X:

struct X
{
    X& operator=(const X&);
    X& operator=(X&&);
};

Now the object code does change:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSEOS_            ## TAILCALL
    .cfi_endproc

Running __ZN1XaSEOS_ through c++filt reveals that X::operator=(X&&) is being called instead of X::operator=(X const&).

And that's all there is to std::move! It completely disappears at run time. Its only impact is at compile-time where it might alter what overload gets called.

Tuesday, June 8, 2021
 
pwaring
answered 6 Months ago
90

You can't copy and move only once, unless you change the design of your classes and turn their constructors into (possibly SFINAE-constrained) templated forwarding constructors (Yakk's answer shows how).

While doing that would make it possible to perform just one move and no copy when rvalues are provided, and one copy and no moves when lvalues are provided, it is an overkill in most situations.

As an alternative to the template-based forwarding constructors, you could provide two constructors, both in your base class and in your derived class: one for rvalue references and one for lvalue references to const. But again, that's an unnecessary complication in most scenarios (and doesn't scale well when the number of arguments increases, since the number of required constructors would increase exponentially).

Moving an std::string is as fast as copying a pointer and an integer (ignoring SSO optimization here), and you should not bother about it unless you have real evidence that this is a bottleneck that prevents your application from meeting its performance requirements (hard to believe).

Therefore, just let your Derived constructor take its argument by value unconditionally, and move it when passing it to the base class's constructor:

class Derived : public Base
{
public:
    Derived(std::string text) : Base(std::move(text)) { }
};

Another option, in case you want (or accept) Derive to inherit all of Base's constructors, is to exploit C++11's inherited constructors like so:

class Derived : public Base
{
public:
    using Base::Base;
//  ^^^^^^^^^^^^^^^^^
};
Saturday, November 13, 2021
 
Saint Robson
answered 2 Weeks ago
78

lapin, your code is fine C++11 code. In C++98/03 your code will probably be efficient due to compiler optimizations, but those optimizations aren't guaranteed. In C++11, those same optimizations will probably still make your return free, but just in case they don't, your string will be moved instead of copied.

So return by value guilt-free! :-)

Minor nit:

It is best practice to declare your values at the point of first use, instead of at the top of a block:

string sValue = ss.str();
return sValue;

Or perhaps even:

return ss.str();

But this is just a minor nit. Your code is fine and efficient.

Tuesday, November 16, 2021
 
Nitesh
answered 2 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share