std::move 或 std::forward 在 C++ 中将通用构造函数分配给成员变量时
std::move or std::forward when assigning universal constructor to member variable in C++
考虑以下 类 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
.
foo2
的构造函数是否总是代表初始化成员变量 foo1<T>
的正确方法,因为需要转发到 foo1 的构造函数?即通过使用 std::forward
.
更新
以下示例使用 std::move
对 foo1
失败:
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 既是引用类型又是值类型。
这些示例都没有使用通用引用(转发引用,现在这样称呼)。
转发引用仅在存在类型推导的情况下形成,但 foo1
和 foo2
的构造函数中的 T&&
未被推导,因此它只是一个右值引用。
因为两者都是右值引用,所以你应该对两者都使用 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) }
{
}
};
在这种情况下,您不应在 foo1
中使用 std::move
,因为客户端代码可以传递一个左值并让对象静默失效:
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) }
{
}
};
求完美转发版本:
- 传递左值 -> 一份
- 传递右值 -> 一步
对于按值传递和移动版本:
- 左值传递 -> 一份,一步
- 传递右值 -> 两步
考虑此代码的性能需要如何以及需要更改和维护的程度,然后根据此选择一个选项。
这取决于你如何推断T
。例如:
template<class T>
foo1<T> make_foo1( T&& t ) {
return std::forward<T>(t);
}
在这种情况下,foo1<T>
中的 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;
};
作为一般规则,这种更简单的技术比完美的转发技术多了 1 个移动,以换取大量更少的代码和复杂性。它允许 {}
初始化构造函数的 T t
参数,这很好。
考虑以下 类 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
.
foo2
的构造函数是否总是代表初始化成员变量 foo1<T>
的正确方法,因为需要转发到 foo1 的构造函数?即通过使用 std::forward
.
更新
以下示例使用 std::move
对 foo1
失败:
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 既是引用类型又是值类型。
这些示例都没有使用通用引用(转发引用,现在这样称呼)。
转发引用仅在存在类型推导的情况下形成,但 foo1
和 foo2
的构造函数中的 T&&
未被推导,因此它只是一个右值引用。
因为两者都是右值引用,所以你应该对两者都使用 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) }
{
}
};
在这种情况下,您不应在 foo1
中使用 std::move
,因为客户端代码可以传递一个左值并让对象静默失效:
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) }
{
}
};
求完美转发版本:
- 传递左值 -> 一份
- 传递右值 -> 一步
对于按值传递和移动版本:
- 左值传递 -> 一份,一步
- 传递右值 -> 两步
考虑此代码的性能需要如何以及需要更改和维护的程度,然后根据此选择一个选项。
这取决于你如何推断T
。例如:
template<class T>
foo1<T> make_foo1( T&& t ) {
return std::forward<T>(t);
}
在这种情况下,foo1<T>
中的 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;
};
作为一般规则,这种更简单的技术比完美的转发技术多了 1 个移动,以换取大量更少的代码和复杂性。它允许 {}
初始化构造函数的 T t
参数,这很好。