Search code examples
c++c++11operator-precedencestdtuplelist-initialization

C++ multiple forwarding of one reference: first copy and then move


Consider the following code, where the same reference is forwarded two times to a base class and used there to construct a tuple:

template<typename ... Ts>
struct Base
{
    template<typename ... _Ts>
    Base(_Ts&& ... ts) : tup{std::forward<_Ts>(ts) ...} {}

    std::tuple <Ts ...> tup;
};

template<typename T>
struct Derived : public Base<T, T>
{
    template<typename _T>
    Derived(_T&& t) : Base<T, T>{t, std::forward<_T>(t)} {}
};

Calling first the base class constructor in Derived as Base<T, T>{t, std::forward<_T>(t)} and thereafter also the tuple constructor using tup{std::forward<Ts>(ts)...} has the following reason:

When t is an rvalue reference, the first tuple argument shall be passed an lvalue-ref to t and thus be constructed through a copy of t, whereas the second tuple element should get an rvalue-ref and therefore, if possible, use a move for construction.

This approach seems to be supported by several questions and answers on SO (e.g. here, here and here) which state that the braced-init list performs to a left-to-right evaluation of its arguments.

However, when I use the above code in a simple example, the actual behavior is (consistently) the opposite of what I expected:

struct A
{
    A() = default;
    A(A const& other) : vec(other.vec) { std::cout<<"copy"<<std::endl; }
    A(A && other) : vec(std::move(other.vec)) { std::cout<<"move"<<std::endl; }

    std::vector<int> vec = std::vector<int>(100);
};

int main()
{
    Derived<A> d(A{});  //prints first "move", then "copy"

    std::cout<<std::get<0>(d.tup).vec.size()<<std::endl;  //prints 0
    std::cout<<std::get<1>(d.tup).vec.size()<<std::endl;  //prints 100
}

Here is the example using gcc on Coliru. (The gcc compiler apparently had a bug once in this connection, but it's about two years since and should be no issue anymore.)

Questions:

  • Where am I wrong on the implementation or the assumptions here?
  • How can the above code be fixed to behave as expected: first copy -- then move?

Solution

  • I'm not sure the order-of-operations on the object initializations matter. Due to the perfect forwarding, no copies or moves are actually made (i.e. only lvalue references and rvalue references are passed around) until the std::tuple constructor is called. And, at that point, it depends on the implementation details of std::tuple.

    Consider if instead of std::tuple you used the following my_tup struct:

    template<typename T1, typename T2>
    struct my_tup
    {
        template <typename A, typename B>
        my_tup(A&& a, B&& b) 
            : t1(std::forward<A>(a)), t2(std::forward<B>(b))
        {
        }
    
        T1 t1;
        T2 t2;
    };
    

    This prints, as expected, "copy" and then "move" (coliru). But if instead you have:

    template<typename T1, typename T2>
    struct my_tup
    {
        template <typename A, typename B>
        my_tup(A&& a, B&& b) 
            : t2(std::forward<B>(b)), t1(std::forward<A>(a))
        {
        }
    
        T2 t2;
        T1 t1;
    };
    

    Then this prints "move", then "copy", as std::tuple does (coliru).

    Likely due to the way the variadic template is expanded, std::tuple must be dealing with the arguments in a right-to-left manner. I am not sure whether this is implementation-dependent or specified in the spec.