Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::forward and constructor with non-const reference argument

In A brief Introduction to Rvalue References, perfect forwarding is proposed as the ideal solution to forward rvalue 5 to a constructor with a non-const reference argument.

But:

#include <memory>
#include <iostream>
#include <utility>

template <class T, class A1>
std::shared_ptr<T> factory(A1&& a1) {
   return std::shared_ptr<T>(new T(std::forward<A1>(a1)));
}

class X {
public:
    X(int& i){
        std::cout<<"X("<<i<<")\n";
    }
};


int main() {
    std::shared_ptr<X> p = factory<X>(5);
}

fails in XCode 4.2 ans G++ 4.6.1 with no known conversion from int to int&, whereas:

template <class T, class A1>
std::shared_ptr<T> factory(A1&& a1) {
   return std::shared_ptr<T>(new T(/*no forwarding*/a1));
}

compiles. What did I get wrong?

like image 491
Klaus Ahrens Avatar asked Dec 02 '11 11:12

Klaus Ahrens


3 Answers

You cannot bind rvalues to non-const lvalue references. The article does not suggest using perfect forwarding for that, because that's not possible. Perfect forwarding forwards lvalues as lvalues, and rvalues as rvalues:

Here, forward preserves the lvalue/rvalue-ness of the argument that was passed to factory. If an rvalue is passed to factory, then an rvalue will be passed to T's constructor with the help of the forward function. Similarly, if an lvalue is passed to factory, it is forwarded to T's constructor as an lvalue.

Since the constructor in your example only takes lvalues, you can only pass lvalues into the factory function. Passing an rvalue will forward it as an rvalue, and that will be ill-formed because there's no way to pass an rvalue to that constructor.

like image 137
R. Martinho Fernandes Avatar answered Oct 21 '22 11:10

R. Martinho Fernandes


perfect forwarding is proposed as the ideal solution to forward rvalue 5 to a constructor with a non-const reference argument.

I don't think perfect forwarding means that. The article, if it is correct, cannot suggest that, even remotely.

Rather, it means it can forward rvalue references as rvalues, so to invoke move-constructor or a constructor/function which takes rvalue references .

So you should try this:

class X {
public:
    X(int& i){
        std::cout<<"X("<<i<<")\n";
    }

    //this is a constructor which takes rvalue references
    X(int&& i){ 
        std::cout<<"X("<<i<<")\n";
    }
};

That is, from factory, the second constructor should be invoked, not the one which you've written.

By the way, in this case, the constructor doesn't makes sense much, because the parameter type int is a fundamental type.

Rvalue referencs as parameter type is used to defined move-constructor and move-assignment of classes which manages resources. If user-defined class doesn't manage resource, then move-semantics doesn't make sense.

like image 44
Nawaz Avatar answered Oct 21 '22 09:10

Nawaz


Ignore rvalue-references for a second, and instead pretend this was allowed:

void modify_int(int& i)
{
    i = 1;
}

void foo(int& x)
{
    modify_int(x); // okay, modify_int references x
}

int i = 7;
foo(i); // makes i = 1

// illegal in standard C++, cannot bind a temporary to a non-const reference
foo(5); // makes the temporary integer equal to 1

You can see that the temporary object gets modified, which is perfectly fine. However, this binding was made illegal in C++ because its usually not desired (it reads as if 5 was being changed to 1, after all).

All rvalue-references do is enable the binding of temporary values to references, but safely because we understand that we're dealing with a value that should be considered temporary:

void modify_int(int& i)
{
    i = 1;
}

void foo(int&& x)
{
    modify_int(x); // okay, modify_int references x
}

int i = 7;
foo(std::move(i)); // makes i = 1 (std::move makes it an rvalue)

// legal in C++11, temporary is bound to rvalue-reference
foo(5); // makes the temporary integer equal to 1

Note that in this version of foo, passing to modify_int is still perfectly fine. Once inside the function, the fact that it was an rvalue-reference instead of an lvalue-reference is irrelevant: we still have an object to refer to. Forwarding is used in templates to preserve the value category:

void test(int& i) {} // lvalue version of test
void test(int&& i) {} // rvalue version of test

template <typename T>
void foo(T&& x)
{
    // if x was an lvalue, forward does nothing;
    // if x was an rvalue, forward std::move's it 
    test(std::forward<T>(x)); 
}

int i = 7;
foo(i); // calls lvalue version of test

foo(5); // calls rvalue version of test

Your code without forwarding is similar to the second snippet in my answer. Once inside the factory function, a1 is just a regular lvalue, and binds to the constructor reference just fine. But with forwarding, it turns back into an rvalue (because factory(5) calls it with an rvalue), which cannot bind to the lvalue-reference, resulting in an error.

like image 33
GManNickG Avatar answered Oct 21 '22 09:10

GManNickG