3

I'm trying to get my head around perfect forwarding in C++, and so I've written the following quick and dirty bit of code to test this.

class CValue
{
public:

    CValue(int i) :
        m_i(i)
    {
        printf("Default constructor called!\r\n");
    }

    CValue(const CValue& src) :
        m_i(src.m_i)
    {
        printf("Copy constructor called!\r\n");
    }

    CValue(CValue&& src) :
        m_i(src.m_i)
    {
        printf("Move constructor called!\r\n");
    }

    CValue& operator=(const CValue& src)
    {
        m_i = src.m_i;
        printf("Copy assignment called!\r\n");
        return *this;
    }

    CValue& operator=(CValue&& src)
    {
        m_i = src.m_i;
        printf("Move assignment called!\r\n");
        return *this;
    }

    int     m_i;
};

template <typename T>
void PerfectForwarding(T&& tValue)
{
    T tValue1 = tValue;
    tValue1.m_i = 10;
}

int _tmain(int argc, _TCHAR* argv[])
{
    CValue v(0);
    PerfectForwarding(v);

    printf("%d\r\n", v.m_i);

    return 0;
}

When I build and run this code as a console application I get the answer 10, when I was expecting 0.

It seems that the line:

T tValue1 = tValue;

in the PerfectForwarding function is being resolved to:

CValue& tValue1 = tValue;

instead of:

CValue tValue1 = tValue;

So the compiler is resolving T to be CValue&, which I didn't expect.

I tried invoking the function from _tmain by explicitly declaring the template parameter type, i.e.

PerfectForwarding<CValue>(v);

but this fails to compile with the following error:

error C2664: 'void PerfectForwarding<CValue>(T &&)' : cannot convert
              argument 1 from 'CValue' to 'CValue &&'
              with
              [
                  T=CValue
              ]
You cannot bind an lvalue to an rvalue reference

I can force the desired behaviour through changing the line in the PerfectForwarding function to the following:

typename std::remove_reference<T>::type tValue1 = tValue;

but I didn't think this should be necessary. By the reference collapsing rules the type of the argument (T&&) should become CValue& (as T&& & -> T&), but T itself should be simply CValue, surely? Is this a bug in the VC12 compiler's handling of rvalue references, or am I misunderstanding something about rvalue references and templates?

I'm using Visual Studio 2013 (VC12 compiler) in debug with all optimisation turned off.

James
  • 33
  • 3
  • In the linked dupe, scan specifically for "universal" (or "forwarding") references. – sehe Jan 19 '15 at 16:16
  • @sehe, I think http://stackoverflow.com/q/14302849/3093378 is a much better match for the dupe, even in terms of its title. The question is not really about move semantics. – vsoftco Jan 19 '15 at 16:19
  • See [Effective Modern C++](http://shop.oreilly.com/product/0636920033707.do) Items 1, 24 and 28 : _... it's the only situation in template type deduction where T is deduced to be a reference_ – Chris Drew Jan 19 '15 at 16:22
  • @jrok, Yes, sorry I was expecting the answer 0, not 4. I've edited the question to correct this. – James Jan 19 '15 at 16:22
  • @vsoftco That looks quite helpful too! You can add it (I think?) (_I don't think that particular one would end up in a natural query to explain the answer to this question this OP has, I think, but yeah it is more concise in scope_) – sehe Jan 19 '15 at 16:22
  • @sehe, don't have enough reputation to add it in OP's post as a possible dupe. Will add it in my answer though. – vsoftco Jan 19 '15 at 16:23
  • @vsoftco I see I can un-mjölnar these days :L – sehe Jan 19 '15 at 16:24

3 Answers3

3

When the compiler sees just T&& it's going to try to deduce T to something that allows the function to be called with whatever you're giving it. So you wind up with T being Cvalue& when called with an lvalue so the reference collapsing (as you've pointed out) can kick in.

This matters when you try to forward the argument. std::forward<T>(v) will either forward the argument as an lvalue or an rvalue depending on what T is. If it has been deduced to an lvalue reference, it will forward it as an lvalue, if it hasn't, it will be forwarded as an rvalue. The type T is the only thing that distinguishes.

By the reference collapsing rules the type of the argument (T&&) should become CValue& (as T&& & -> T&), but T itself should be simply CValue, surely?
If called with an lvalue it goes (T&& v) "okay, I can't bind an rvalue reference to an lvalue, but if I make T itself be an lvalue reference then this works." T is deduced to be Cvalue& so that (T&& v) expands to (Cvalue& && v). Now the reference is collapsed (Cvalue& v). The type T must be an lvalue-reference type for this to work.

If you provide the template parameter explicitly then you're not really addressing the issue. The first way around this is the remove_reference as you've found. You could also use auto which makes more sense since this is generic programming

auto tValue1 = tValue;

In any case, the auto won't deduce to a reference. For what you've provided, you'd be better off using a const lvalue reference which can bind to both lvalues and rvalues.

template <typename T>
void PerfectForwarding(const T& tValue)
{
    T tValue1 = tValue;
    tValue1.m_i = 10;
}

this doesn't allow for forwarding, but it's not a forwarding function as-is anyway.

If you wanted to handle lvalues differently you could provide two overloads

template <typename T>
void PerfectForwarding(T& tValue);

template <typename T>
void PerfectForwarding(T&& tValue);

When called with an lvalue, the former will be preferred.

Ryan Haining
  • 35,360
  • 15
  • 114
  • 174
  • 2
    Though it should be mentioned that `auto&&` is the perfect forwarding version of `auto`! – OmnipotentEntity Jan 19 '15 at 16:03
  • @OmnipotentEntity: Then what's `decltype(auto)` ? – Ben Voigt Jan 19 '15 at 16:05
  • 1
    @BenVoigt whatever the rhs was declared to be. – Ryan Haining Jan 19 '15 at 16:06
  • @BenVoigt `auto` deduces the type of expression by the rules of template-argument deduction and `decltype(auto)` deduces through `decltype()`. – David G Jan 19 '15 at 16:08
  • @0x499602D2: Yes.... and isn't `decltype(auto)`, not `auto&&`, the perfect forwarding version of `auto`? – Ben Voigt Jan 19 '15 at 16:15
  • @BenVoigt `auto&&` will always bind a reference. but something like `int a; decltype(auto) b = a;` will not, because `a` is declared as an `int`, not a reference – Ryan Haining Jan 19 '15 at 16:16
  • @RyanHaining: Exactly, `auto&&` isn't perfect forwarding because it doesn't preserve the value category, it forces everything to be a reference. – Ben Voigt Jan 19 '15 at 16:17
  • 1
    @BenVoigt in the case of forwarding, isn't it really all about getting the right kind of reference to the object? A forwarding function always binds a reference, it doesn't only bind in the case that the argument to the function was declared as a reference. – Ryan Haining Jan 19 '15 at 16:18
  • @BenVoigt "it forces everything to be a reference" - which is precisely what perfect forwarding does in functions. Depends on your exact definition of the vague term "perferct forwarding version of `auto`," I guess. – Angew is no longer proud of SO Jan 19 '15 at 16:19
1

The behaviour is correct. Perfect forwarding is implemented by deducing an lvalue reference for the type when the argument is an lvalue.

You're saying

By the reference collapsing rules the type of the argument (T&&) should become CValue& (as T&& & -> T&), but T itself should be simply CValue, surely?

But where would the & in T&& & come from if T was just CValue? The entire reason why T&& works as a forwarding reference is that the T becomes an lvalue reference, CValue& in your case, and then reference collapsing gets T && -> CValue & && -> CValue &.

The relevant section of the standard is C++11 14.8.2.1/3 (P is the function template parameter type and A s the type of the argument in the call, defined in 14.8.2.1/1):

If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cv-unqualified template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

(Emphasis mine)

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
0

The compiler does the right thing. In a line like

template<typename T>
void f(T&& param){}

T is being deduced as reference if you pass a lvalue, and a simple type if you pass a rvalue. So int x; f(x); deduces T as int&, and f(1) deduces T as int.

That's how perfect forwarding works. Basically, you have the following rules for reference collapsing:

& &   -> &
& &&  -> &
&& &  -> &
&& && -> &&

See also

Syntax for universal references

for more details.

Community
  • 1
  • 1
vsoftco
  • 55,410
  • 12
  • 139
  • 252