views:

148

answers:

3

Given GMan's deliciously evil auto_cast utility function concocted here, I've been trying to figure out why it doesn't compile for me when I'm trying to auto_cast from an rvalue (on MSVC 10.0).

Here's the code that I'm using:

template <typename T>
class auto_cast_wrapper : boost::noncopyable
{
  public:
    template <typename R>
    friend auto_cast_wrapper<R> auto_cast(R&& pX);

    template <typename U>
    operator U() const
    {
      return static_cast<U>( std::forward<T>(mX) );
    }

  private:
    //error C2440: 'initializing': cannot convert from 'float' to 'float &&'
    auto_cast_wrapper(T&& pX) : mX(pX) { }

    T&& mX;
};

template <typename R>
auto_cast_wrapper<R> auto_cast(R&& pX)
{
  return auto_cast_wrapper<R>( std::forward<R>(pX) );
}

int main()
{
  int c = auto_cast( 5.0f );  // from an rvalue
}

To the best of my ability I've tried to follow the C++0x reference collapsing rules and the template argument deduction rules outlined here, and as far as I can tell the code given above should work.

Recall that in pre-0x C++, it is not allowed to take a reference to a reference: something like A& & causes a compile error. C++0x, by contrast, introduces the following reference collapsing rules:

  • A& & becomes A&
  • A& && becomes A&
  • A&& & becomes A&
  • A&& && becomes A&&

The second rule is a special template argument deduction rule for function templates that take an argument by rvalue reference to a template argument:

template<typename T>  
void foo(T&&);

Here, the following rules apply:

  1. When foo is called on an lvalue of type A, then T resolves to A& and hence, by the reference collapsing rules above, the argument type effectively becomes A&.
  2. When foo is called on an rvalue of type A, then T resolves to A, and hence the argument type becomes A&&.

Now when I mouse over the call to auto_cast( 5.0f ), the tooltip correctly displays its return value as auto_cast_wrapper<float>. This meaning that the compiler has correctly followed rule 2:

When foo is called on an rvalue of type A, then T resolves to A.

So since we have an auto_cast_wrapper<float>, the constructor should instantiate to take a float&&. But the error message seems to imply that it instantiates to take a float by value.

error showing tooltip

Here's the full error message, showing again that T=float correctly yet the T&& parameter becomes T?

 main.cpp(17): error C2440: 'initializing' : cannot convert from 'float' to 'float &&'
     You cannot bind an lvalue to an rvalue reference
     main.cpp(17) : while compiling class template member function 'auto_cast_wrapper<T>::auto_cast_wrapper(T &&)'
     with
     [
         T=float
     ]
     main.cpp(33) : see reference to class template instantiation 'auto_cast_wrapper<T>' being compiled
     with
     [
         T=float
     ]

Any thoughts?

+3  A: 

You forgot to std::forward the T&& argument to the auto_cast_wrapper constructor. This breaks the forwarding chain. The compiler now gives a warning but it seems to work fine.

template <typename T>
class auto_cast_wrapper
{
  public:
    template <typename R>
    friend auto_cast_wrapper<R> auto_cast(R&& pX);

    template <typename U>
    operator U() const
    {
      return static_cast<U>( std::forward<T>(mX) );
    }

  private:
    //error C2440: 'initializing': cannot convert from 'float' to 'float &&'
    auto_cast_wrapper(T&& pX) : mX(std::forward<T>(pX)) { }

    auto_cast_wrapper(const auto_cast_wrapper&);
    auto_cast_wrapper& operator=(const auto_cast_wrapper&);

    T&& mX;
};

template <typename R>
auto_cast_wrapper<R> auto_cast(R&& pX)
{
  return auto_cast_wrapper<R>( std::forward<R>(pX) );
}

float func() {
    return 5.0f;
}

int main()
{

  int c = auto_cast( func() );  // from an rvalue
  int cvar = auto_cast( 5.0f );

  std::cout << c << "\n" << cvar << "\n";
  std::cin.get();
}

Prints a pair of fives.

DeadMG
dvide
@dvide: I tested it with some rvalues - for example, returned by value from a function - and it seems to work.
DeadMG
@DeadMG: What is your warning? And on what compiler? I get the warning: reference member is initialized to a temporary that doesn't persist after the constructor exits. And when I test it the value of `c` is messed up. It could be that it just appears to work, given that it's technically undefined?
dvide
@dvide: I get that warning, but the value of C was always correct when I couted it. MSVC10. Remember that different compilers are in the different stages of Standard conformance and MSVC10 may be less or more conformant than your compiler.
DeadMG
@DeadMG: As I say, I'm also on MSVC10. I believe the warning is correct and shouldn't just be ignored because it seems to work on your computer. The exact same code prints a pair of garbage numbers for me in debug mode and a pair of fives in release mode.
dvide
I'll accept this as it does answer the original question, but I've extended my question with a follow-up regarding this potentially undefined behaviour.
dvide
@dvide: You're right - I only pulled it in release mode, thought I was in debug. I also get garbage in debug mode.
DeadMG
+2  A: 

Sorry for posting untested code. :)

DeadMG is correct that the argument should be forwarded as well. I believe the warning is false and the MSVC has a bug. Consider from the call:

auto_cast(T()); // where T is some type

T() will live to the end of the full expression, which means the auto_cast function, the auto_cast_wrapper's constructor, and the user-defined conversion are all referencing a still valid object.

(Since the wrapper can't do anything but convert or destruct, it cannot outlive the value that was passed into auto_cast.)

I fix might be to make the member just a T. You'll be making a copy/move instead of casting the original object directly, though. But maybe with compiler optimization it goes away.


And no, the forwarding is not superfluous. It maintains the value category of what we're automatically converting:

struct foo
{
    foo(int&) { /* lvalue */ }
    foo(int&&) { /* rvalue */ }
};

int x = 5;
foo f = auto_cast(x); // lvalue
foo g = auto_cast(7); // rvalue

And if I'm not mistaken the conversion operator shouldn't be (certainly doesn't need to be) marked const.

GMan
Thanks GMan. I didn't know that it should last until the end of the entire expression. That makes sense. I guess this is just a bug then. And thanks for the perfect forwarding clarification. That's pretty cool.
dvide
GMan
@GMan: Yeah I was a little confused with the lifetime incongruity of the temporary between the two versions. As far as I could tell they should be the same, but I'm still getting used to rvalue references so I'm very wary of jumping to any conclusions =)
dvide
+1  A: 

The reason it doesn't compile is the same reason for why this doesn't compile:

float rvalue() { return 5.0f }

float&& a = rvalue();
float&& b = a; // error C2440: 'initializing' : cannot convert from 'float' to 'float &&'  

As a is itself an lvalue it cannot be bound to b. In the auto_cast_wrapper constructor we should have used std::forward<T> on the argument again to fix this. Note that we can just use std::move(a) in the specific example above, but this wouldn't cover generic code that should work with lvalues too. So the auto_cast_wrapper constructor now becomes:

template <typename T>
class auto_cast_wrapper : boost::noncopyable
{
  public:
    ...

  private:
    auto_cast_wrapper(T&& pX) : mX( std::forward<T>(pX) ) { }

    T&& mX;
};

Unfortunately it seems that this now exhibits undefined behaviour. I get the following warning:

warning C4413: 'auto_cast_wrapper::mX' : reference member is initialized to a temporary that doesn't persist after the constructor exits

It appears that the literal goes out of scope before the conversion operator can be fired. Though this might be just a compiler bug with MSVC 10.0. From GMan's answer, the lifetime of a temporary should live until the end of the full expression.

dvide