1

考虑以下代码,其中相同的引用被转发两次到一个基类并在那里构造一个元组:

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)} {}
};

Derived首先调用as中的基类构造函数,Base<T, T>{t, std::forward<_T>(t)}然后调用元组构造函数 usingtup{std::forward<Ts>(ts)...}具有以下原因:

t是一个右值引用时,第一个元组参数应该传递一个左值引用t,因此通过一个副本来构造t,而第二个元组元素应该得到一个右值引用,因此,如果可能的话,使用移动进行构造。

这种方法似乎得到了关于 SO(例如, hereherehere)的几个问题和答案的支持,这些问题和答案表明braced-init 列表对其参数进行从左到右的评估。

但是,当我在一个简单的示例中使用上述代码时,实际行为(始终)与我的预期相反:

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
}

这是在 Coliru 上使用 gcc 的示例。(gcc 编译器在这方面显然有过一次错误,但距今已有大约两年了,应该不再有问题了。)

问题:

  • 我在这里的实施或假设哪里错了?
  • 如何修复上述代码以使其行为符合预期:首先复制——然后移动?
4

2 回答 2

2

您应该明确地进行复制,否则您传递一个稍后由复制使用的引用(但是,使用别名,可能已被移动):

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

演示

于 2016-02-11T18:44:06.147 回答
1

我不确定对象初始化的操作顺序是否重要。std::tuple由于完美转发,在调用构造函数之前,实际上不会进行任何复制或移动(即仅传递左值引用和右值引用) 。并且,在这一点上,它取决于std::tuple.

考虑是否代替std::tuple您使用以下my_tup结构:

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;
};

正如预期的那样,这将打印“复制”然后“移动”(coliru)。但是,如果您有:

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;
};

然后这会打印“移动”,然后是“复制”,就像std::tuple( coliru ) 一样。

可能由于可变参数模板的扩展方式,std::tuple必须以从右到左的方式处理参数。我不确定这是否依赖于实现或在规范中指定。

于 2016-02-11T18:39:29.013 回答