Why parameters of universal reference needs to be

2019-04-08 01:07发布

In the lecture about universal references, Scott Meyers (at approximately 40th minute) said that objects that are universal references should be converted into real type, before used. In other words, whenever there is a template function with universal reference type, std::forward should be used before operators and expressions are used, otherwise a copy of the object might be made.

My understanding of this is in the following example :

#include <iostream>

struct A
{
  A() { std::cout<<"constr"<<std::endl; }
  A(const A&) { std::cout<<"copy constr"<<std::endl; }
  A(A&&) { std::cout<<"move constr"<<std::endl; }
  A& operator=(const A&) { std::cout<<"copy assign"<<std::endl; return *this; }
  A& operator=(A&&) { std::cout<<"move assign"<<std::endl; return *this; }

  ~A() { std::cout<<"destr"<<std::endl; }

  void bar()
  {
    std::cout<<"bar"<<std::endl;
  }
};

A getA()
{
  A a;
  return a;
}

template< typename T >
void callBar( T && a )
{
  std::forward< T >( a ).bar();
}

int main()
{
  {
    std::cout<<"\n1"<<std::endl;
    A a;
    callBar( a );
  }

  {
    std::cout<<"\n2"<<std::endl;
    callBar( getA() );
  }
}

As expected, the output is :

1
constr
bar
destr

2
constr
move constr
destr
bar
destr

The question really is why is this needed?

std::forward< T >( a ).bar();

I tried without std::forward, and it seems to work fine (the output is the same).

Similarly, why he recommends to use move inside the function with rvalue? (the answer is the same as for std::forward)

void callBar( A && a )
{
  std::move(a).bar();
}

I understand that both std::move and std::forward are just casts to appropriate types, but are these casts really needed in the above example?

Bonus : how can the example be modified to produce the copy of the object that is passed to that function?

4条回答
Evening l夕情丶
2楼-- · 2019-04-08 01:13

What is said in the lecture is this :

void doWork( Widget&& param )
{
  ops and exprs using std::move(param)
}

SM: What this means is : if you see code that takes a rvalue reference, and you see use of that parameter without being wrapped by move, it is highly suspect.

After some thought, I realized that it is correct (as expected). Changing the callBar function in the original example to this demonstrate the point :

void reallyCallBar( A& la )
{
  std::cout<<"lvalue"<<std::endl;
  la.bar();
}

void reallyCallBar( A&& ra )
{
  std::cout<<"rvalue"<<std::endl;
  ra.bar();
}

template< typename T >
void callBar( T && a )
{
  reallyCallBar( std::forward< T >( a ) );
}

If the std::forward wasn't used in callBar, then the reallyCallBar( A& ) would be used. Because a in callBar is a lvalue reference. std::forward makes it a rvalue, when the universal reference is the rvalue reference.

Next modification proves the point even further :

void reallyCallBar( A& la )
{
  std::cout<<"lvalue"<<std::endl;
  la.bar();
}

void reallyCallBar( A&& ra )
{
  std::cout<<"rvalue"<<std::endl;
  reallyCallBar( ra );
}

template< typename T >
void callBar( T && a )
{
  reallyCallBar( std::forward< T >( a ) );
}

Since std::move is not used in the reallyCallBar( A&& ra ) function, it doesn't enter the endless loop. Instead it calls the version taking lvalue reference.

Therefore (as explained in the lecture) :

  • std::forward must be used on universal references
  • std::move must be used on rvalue references
查看更多
劫难
3楼-- · 2019-04-08 01:18
void callBar( A && a )
{
  std::move(a).bar();
}

In the case where you have an rvalue reference as a parameter, which can only bind to an rvalue you normally want to use move semantics to move from this this rvalue and take it's guts out.

The parameter itself is an lvalue, because it is a named thing. You can take it's address.

So in order to make it an rvalue again and be able to move from it, you apply std::move to it. If you were literally just calling a function on a passed parameter, I don't see why you'd have a parameter that is an rvalue reference.

You only want to pass an rvalue reference if you are going to move from this inside your function, which is why you then have to use std::move.

Your example here doesn't actually make much sense in that respect.

查看更多
小情绪 Triste *
4楼-- · 2019-04-08 01:23

There are two different uses for && on a parameter to a function. For an ordinary function it means that the argument is an rvalue reference; for a template function it means that it can be either an rvalue reference or an lvalue reference:

template <class T> void f(T&&); // rvalue or lvalue
void g(T&&);                    // rvalue only
void g(T&)                      // lvalue only

void h() {
    C c;
    f(c);            // okay: calls f(T&)
    f(std::move(c)); // okay: calls f(T&&)
    g(c);            // error: c is not an rvalue
    g(std::move(c)); // okay: move turns c into an rvalue
}

Inside f and g, applying std::forward to such an argument preserves the lvalue- or rvalue-ness of the argument, so in general that's the safest way to forward an argument to another function.

查看更多
我欲成王,谁敢阻挡
5楼-- · 2019-04-08 01:24

It's needed because bar() might be overloaded separately for rvalues and lvalues. That means that it might do something differently, or flat out not be allowed, depending on if you correctly described a as an lvalue or an rvalue, or just blindly treated it like an lvalue. Right now, most users don't use this functionality and don't have exposure to it because the most popular compilers don't support it - even GCC 4.8 doesn't support rvalue *this. But it is Standard.

查看更多
登录 后发表回答