C++多次转发一个引用:先复制再移动
C++ multiple forwarding of one reference: first copy and then move
考虑以下代码,其中相同的引用被转发两次到一个基 class 并在那里用于构造一个元组:
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
中的基础 class 构造函数作为 Base<T, T>{t, std::forward<_T>(t)}
然后调用使用 tup{std::forward<Ts>(ts)...}
的元组构造函数有以下原因:
当 t
是右值引用时,第一个元组参数应传递一个左值引用给 t
,因此通过 t
的副本构造,而第二个元组参数元组元素应该得到一个右值引用,因此,如果可能的话,使用一个移动来构建。
这种方法似乎得到了关于 SO 的几个问题和答案的支持(例如 here, here and here),这些问题和答案表明 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
}
这里是the example using gcc on Coliru。 (gcc 编译器显然在这方面有过一次错误,但已经过去两年了,应该不再是问题了。)
问题:
- 这里的实施或假设我哪里错了?
- 如何固定上述代码以按预期运行:先复制 -- 然后移动?
我不确定对象初始化的操作顺序是否重要。由于完美转发,在调用 std::tuple
构造函数之前,实际上不会进行任何复制或移动(即仅传递左值引用和右值引用)。并且,到那时,它取决于 std::tuple
的实现细节。
考虑是否使用以下 my_tup
结构而不是 std::tuple
:
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;
};
这会按预期打印 "copy",然后是 "move" (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;
};
然后打印 "move",然后打印 "copy",就像 std::tuple
那样 (coliru)。
可能由于可变参数模板的扩展方式,std::tuple
必须以从右到左的方式处理参数。我不确定这是依赖于实现还是在规范中指定。
您应该明确地进行复制,否则您将传递一个稍后由复制使用的引用(但是,使用别名,可能已被移动):
template<typename U>
struct Derived : public Base<U, U>
{
template<typename T>
Derived(T&& t) : Base<U, U>{T(t), std::forward<T>(t)} {}
};
考虑以下代码,其中相同的引用被转发两次到一个基 class 并在那里用于构造一个元组:
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
中的基础 class 构造函数作为 Base<T, T>{t, std::forward<_T>(t)}
然后调用使用 tup{std::forward<Ts>(ts)...}
的元组构造函数有以下原因:
当 t
是右值引用时,第一个元组参数应传递一个左值引用给 t
,因此通过 t
的副本构造,而第二个元组参数元组元素应该得到一个右值引用,因此,如果可能的话,使用一个移动来构建。
这种方法似乎得到了关于 SO 的几个问题和答案的支持(例如 here, here and here),这些问题和答案表明 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
}
这里是the example using gcc on Coliru。 (gcc 编译器显然在这方面有过一次错误,但已经过去两年了,应该不再是问题了。)
问题:
- 这里的实施或假设我哪里错了?
- 如何固定上述代码以按预期运行:先复制 -- 然后移动?
我不确定对象初始化的操作顺序是否重要。由于完美转发,在调用 std::tuple
构造函数之前,实际上不会进行任何复制或移动(即仅传递左值引用和右值引用)。并且,到那时,它取决于 std::tuple
的实现细节。
考虑是否使用以下 my_tup
结构而不是 std::tuple
:
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;
};
这会按预期打印 "copy",然后是 "move" (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;
};
然后打印 "move",然后打印 "copy",就像 std::tuple
那样 (coliru)。
可能由于可变参数模板的扩展方式,std::tuple
必须以从右到左的方式处理参数。我不确定这是依赖于实现还是在规范中指定。
您应该明确地进行复制,否则您将传递一个稍后由复制使用的引用(但是,使用别名,可能已被移动):
template<typename U>
struct Derived : public Base<U, U>
{
template<typename T>
Derived(T&& t) : Base<U, U>{T(t), std::forward<T>(t)} {}
};