考虑以下课程foo1
和foo2
template <typename T>
struct foo1
{
T t_;
foo1(T&& t) :
t_{ std::move(t) }
{
}
};
template <typename T>
struct foo2
{
foo1<T> t_;
foo2(T&& t) :
t_{ std::forward<T>(t) }
{
}
};
foo1
的构造函数是否始终表示初始化成员变量T
的正确方法?即使用std::move
。
由于需要转发到foo1的构造函数,foo2
的构造函数是否始终表示初始化成员变量foo1<T>
的正确方法?即使用std::forward
。
更新
以下示例使用foo1
{/ 1}} std::move
失败。
template <typename T>
foo1<T> make_foo1(T&& t)
{
return{ std::forward<T>(t) };
}
struct bah {};
int main()
{
bah b;
make_foo1(b); // compiler error as std::move cannot be used on reference
return EXIT_SUCCESS;
}
这是一个问题,因为我希望T既是引用类型又是值类型。
答案 0 :(得分:10)
这些示例都没有使用通用引用(转发引用,因为它们现在被称为)。
转发引用仅在存在类型推导时形成,但T&&
和foo1
的构造函数中的foo2
不会被推导出来,因此它只是一个右值引用。
由于两者都是右值引用,因此您应在两者上使用std::move
。
如果要使用转发引用,则应使构造函数具有推导出的模板参数:
template <typename T>
struct foo1
{
T t_;
template <typename U>
foo1(U&& u) :
t_{ std::forward<U>(u) }
{
}
};
template <typename T>
struct foo2
{
foo1<T> t_;
template <typename U>
foo2(U&& u) :
t_{ std::forward<U>(u) }
{
}
};
在这种情况下,您不应在std::move
中使用foo1
,因为客户端代码可以传递左值并使对象无声地失效:
std::vector<int> v {0,1,2};
foo1<std::vector<int>> foo = v;
std::cout << v[2]; //yay, undefined behaviour
更简单的方法是将值和无条件std::move
带入存储:
template <typename T>
struct foo1
{
T t_;
foo1(T t) :
t_{ std::move(t) }
{
}
};
template <typename T>
struct foo2
{
foo1<T> t_;
foo2(T t) :
t_{ std::move(t) }
{
}
};
完美的转发版本:
对于传递值和移动版本:
考虑这段代码需要具备的性能,以及需要更改和维护多少代码,并根据它选择一个选项。
答案 1 :(得分:0)
这取决于您推断T
的方式。例如:
template<class T>
foo1<T> make_foo1( T&& t ) {
return std::forward<T>(t);
}
在这种情况下,T
中的foo1<T>
是转发参考,您的代码将无法编译。
std::vector<int> bob{1,2,3};
auto foo = make_foo1(bob);
上述代码默默地从bob
移动到构造函数中的std::vector<int>&
到foo1<std::vector<int>&>
。
对foo2
执行相同操作会有效。您将获得foo2<std::vector<int>&>
,并且会引用bob
。
编写模板时,必须考虑类型T
的参考含义。如果您的代码不支持将其作为参考,请考虑使用static_assert
或SFINAE来阻止该情况。
template <typename T>
struct foo1 {
static_assert(!std::is_reference<T>{});
T t_;
foo1(T&& t) :
t_{ std::move(t) }
{
}
};
现在,此代码会生成合理的错误消息。
你可能认为现有的错误信息没问题,但只是因为我们搬进了T
。
template <typename T>
struct foo1 {
static_assert(!std::is_reference<T>{});
foo1(T&& t)
{
auto internal_t = std::move(t);
}
};
此处只有static_assert
确保我们的T&&
实际上是一个左值。
但是这个问题的理论清单已经足够了。你有一个具体的。
最后这可能是你想要的:
template <class T> // typename is too many letters
struct foo1 {
static_assert(!std::is_reference<T>{});
T t_;
template<class U,
class dU=std::decay_t<U>, // or remove ref and cv
// SFINAE guard required for all reasonable 1-argument forwarding
// reference constructors:
std::enable_if_t<
!std::is_same<dU, foo1>{} && // does not apply to `foo1` itself
std::is_convertible<U, T> // fail early, instead of in body
,int> = 0
>
foo1(U&& u):
t_(std::forward<U>(u))
{}
// explicitly default special member functions:
foo1()=default;
foo1(foo1 const&)=default;
foo1(foo1 &&)=default;
foo1& operator=(foo1 const&)=default;
foo1& operator=(foo1 &&)=default;
};
或者,更简单的案例在99/100案例中同样出色:
template <class T>
struct foo1 {
static_assert(!std::is_reference<T>{});
T t_;
foo1(T t) :
t_{ std::move(t) }
{}
// default special member functions, just because I never ever
// want to have to memorize the rules that makes them not exist
// or exist based on what other code I have written:
foo1()=default;
foo1(foo1 const&)=default;
foo1(foo1 &&)=default;
foo1& operator=(foo1 const&)=default;
foo1& operator=(foo1 &&)=default;
};
作为一般规则,这种更简单的技术比完美的转发技术产生更多的移动,以换取大量的代码和复杂性。它允许{}
初始化构造函数的T t
参数,这很好。