移动语义(Move Semantics)与 enable_if<>
移动语义(Move Semantics)无疑是 C++11 最重要的特性,可以用于优化拷贝和赋值操作。本章将讨论下移动语义对模板设计的影响。移动语义和完美转发相关文章也贴在这里,供参考:
Item 23: Understand std::move and std::forward.
Item 24: Distinguish universal references from rvalue references.
Item 25: Use std::move on rvalue references, std::forward on universal references.
Item 26: Avoid overloading on universal references.
Item 27: Familiarize yourself with alternatives to overloading on universal references.
Item 28: Understand reference collapsing.
Item 29: Assume that move operations are not present, not cheap, and not used.
Item 30: Familiarize yourself with perfect forwarding failure cases.
完美转发
将一个函数实参的如下基本特性转发给另一个函数:
- 可修改的对象转发后应该仍可以被修改。
- 常量对象应该作为只读对象转发。
- 可移动的对象应该作为可移动对象转发。
如果不使用模板,你需要重载上面的所有情况,假设转发 f()
的参数给 g()
:
#include <utility>
#include <iostream>
class X {
...
};
void g (X&) {
std::cout << "g() for variable\n";
}
void g (X const&) {
std::cout << "g() for constant\n";
}
void g (X&&) {
std::cout << "g() for movable object\n";
}
// let f() forward argument val to g():
void f (X& val) {
g(val); // val is non-const lvalue => calls g(X&)
}
void f (X const& val) {
g(val); // val is const lvalue => calls g(X const&)
}
void f (X&& val) {
g(std::move(val)); // val is non-const lvalue => needs std::move() to call g(X&&)
}
int main()
{
X v; // create variable
X const c; // create constant
f(v); // f() for nonconstant object calls f(X&) => calls g(X&)
f(c); // f() for constant object calls f(X const&) => calls g(X const&)
f(X()); // f() for temporary calls f(X&&) => calls g(X&&)
f(std::move(v)); // f() for movable variable calls f(X&&) => calls g(X&&)
}
需要注意的是第三个 f()
,虽然 val
被申明为一个右值引用,但它本身依然是一个非常量左值,如果不使用 std::move
进行转换,将会调用第一个 f()
。
void f (X&& val) {
g(std::move(val)); // val is non-const lvalue => needs std::move() to call g(X&&)
}
C++11 引入的万能引用(Universal Reference)和完美转发(Perfect Forward)机制可以大大简化代码:
template<typename T>
void f (T&& val) {
g(std::forward<T>(val)); // perfect forward val to g()
}
使用万能引用和完美转发版本的 f()
则可以完美替代上述三个版本的 f()
。此处需要注意:对于模板参数 T
的 T&&
和一个特定类型 X
的 X&&
则完全不同。
- 对于特定类型
X
的X&&
是一个右值引用,它只能绑定到一个可移动的对象(a prvalue, such as a temporary object, and an xvalue, such as an object passed with std::move())上。 - 对于模板参数
T
的T&&
是一个万能引用(universal reference,or called perfect reference),它可以绑定到一个可变的、不可变的、可移动的对象上。
T
必须是模板参数,模板的依赖参数不是万能引用。例如 T::iterator&&
只是一个右值引用,而非万能引用。
借助可变参数模板,完美转发也可以转发任意数量参数:
template<typename... Ts>
void f(Ts&&... args)
{
g(std::forward<Ts>(args)...);
}
特殊成员函数模板
成员函数模板也可以用于特殊的成员函数,例如构造函数,但可能产生一些奇怪的行为。
考虑下面代码:
#include <utility>
#include <string>
#include <iostream>
class Person
{
private:
std::string name;
public:
// constructor for passed initial name:
explicit Person(std::string const& n) : name(n) {
std::cout << "copying string-CONSTR for '" << name << "'\n";
}
explicit Person(std::string&& n) : name(std::move(n)) {
std::cout << "moving string-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person (Person const& p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person (Person&& p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
int main()
{
std::string s = "sname";
Person p1(s); // init with string object => calls copying string-CONSTR
Person p2("tmp"); // init with string literal => calls moving string-CONSTR
Person p3(p1); // copy Person => calls COPY-CONSTR
Person p4(std::move(p1)); // move Person => calls MOVE-CONST
}
如果我们使用万能引用和完美转发机制重写前两个 std::string
的构造函数:
#include <iostream>
class Person
{
private:
std::string name;
public:
// generic constructor for passed initial name:
template<typename STR>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {
std::cout << "TMPL-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person (Person const& p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person (Person&& p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
int main()
{
std::string s = "sname";
Person p1(s); // init with string object => calls TMPL-CONSTR
Person p2("tmp"); // init with string object => calls TMPL-CONSTR
Person p3(p1); // ERROR
Person p4(std::move(p1)); // OK: move Person => calls MOVE-CONST
Person const p2c("ctmp"); // init constant object with string literal
Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
}
p1
、p2
、p4
的构造函数调用符合我们的预期,但是 p3 的构造将失败。这是因为完美转发构造函数的模板比拷贝构造函数更匹配 non-const
左值,于是调用完美转换构造函数,导致使用 Person
类型初始化 std::string
的错误。
Person p3(p1);
而拷贝一个 const 的类型的 Person,则不会有问题。这是因为当模板函数和普通函数都能完美匹配时,则优先调用普通函数。
Person const p2c("ctmp"); // init constant object with string literal
Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
对于上面问题的一个解决方案是补充一个非 const 参数的拷贝构造函数:
Person(Person&);
但这不是一个完美的解决方案,对于继承类,完美转发的成员模板构造函数将更加匹配。可以参考文章开头的参考文章 Item 26: Avoid overloading on universal references. 的相关介绍。完美的解决方案是使用 enable_if<>
来禁用某些参数类型的实例化。
使用 enable_if<> 禁用模板
C++11 引入 enable_if<>
,使模板根据编译期条件进行实例化。例如:
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
}
如果表达式 (sizeof(T) > 4
为 false
,该定义将被忽略,即模板不会被实例化。否则,才会实例化。
std::enable_if
是一个 type traits
,编译时表达式作为首个模板实参传递。若表达式为 false
,则 enable_if::type
未定义,由于被称为 SFINAE(substitution failure is not an error)的模板特性,这个带有 enable_if
的函数模板将被忽略。若表达式为 true
,enable_if::type
产生一个类型,若有第二个实参,则类型为第二个实参类型,否则为 void
。
template<typename T>
std::enable_if<(sizeof(T) > 4), T>::type
foo() {
return T();
}
C++14 为所有的 type traits
提供了别名模板。例如 enable_if::type
可以简写为 enable_if_t
,则上述代码可以简化为:
template<typename T>
typename std::enable_if_t<(sizeof(T) > 4)>
foo() {
}
template<typename T>
typename std::enable_if_t<(sizeof(T) > 4), T>
foo() {
return T();
}
为了增加可读性,可以将 enable_if
作为第二个模板参数:
template<typename T,
typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}
如果 (sizeof(T) > 4
为 true
,则可扩展为:
template<typename T,
typename = void>
void foo() {
}
还可以使用 using
进一步简化:
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
template<typename T,
typename = EnableIfSizeGreater4<T>>
void foo() {
}
使用 enable_if<>
对于前面的问题,我们期望的是 std::string
或者可以转为为 std::string
类型的参数才匹配到完美转发构造函数。
template<typename STR>
Person(STR&& n);
借助 enable_if<>
和 std::is_convertible
,并利用 C++17 的写法:std::is_convertible_v
替代 std::is_convertible::value
,实现如下:
template<typename STR,
typename = std::enable_if_t<
std::is_convertible_v<STR, std::string>>>
Person(STR&& n);
如果 STR
是可以转换为 std::string
类型,则上述声明可以扩展为:
template<typename STR,
typename = void>
Person(STR&& n);
同样地,可以借助 using
简化为:
template<typename T>
using EnableIfString = std::enable_if_t<
std::is_convertible_v<T, std::string>>;
...
template<typename STR, typename = EnableIfString<STR>>
Person(STR&& n);
这里给出完整代码:
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>
template<typename T>
using EnableIfString = std::enable_if_t<
std::is_convertible_v<T,std::string>>;
class Person
{
private:
std::string name;
public:
// generic constructor for passed initial name:
template<typename STR, typename = EnableIfString<STR>>
explicit Person(STR&& n)
: name(std::forward<STR>(n)) {
std::cout << "TMPL-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person (Person const& p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person (Person&& p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
int main()
{
std::string s = "sname";
Person p1(s); // init with string object => calls TMPL-CONSTR
Person p2("tmp"); // init with string literal => calls TMPL-CONSTR
Person p3(p1); // OK => calls COPY-CONSTR
Person p4(std::move(p1)); // OK => calls MOVE-CONST
}
也可以用 std::is_constructible
替代 std::is_convertible
,但 std::is_convertible
判断的是类型可以隐式转换,而 std::is_constructible
判断的是显式转换,实参顺序相反。
template<typename T>
using EnableIfString = std::enable_if_t<std::is_constructible_v<std::string, T>>;
禁用特殊成员函数
不能是 enable_if
禁用预定义的拷贝构造函数、赋值运算符、移动构造函数。例如:
class C {
public:
template<typename T>
C (T const&) {
std::cout << "tmpl copy constructor\n";
}
...
};
C x;
C y{x}; // still uses the predefined copy constructor (not the member template)
一个解决办法是使用 =delete
标记预定义的特殊成员函数。
class C
{
public:
...
// user-define the predefined copy constructor as deleted
// (with conversion to volatile to enable better matches)
C(C const volatile&) = delete;
// implement copy constructor template with better match:
template<typename T>
C(T const&) {
std::cout << "tmpl copy constructor\n";
}
...
};
C x;
C y{x}; // uses the member template
此时就可以用 enable_if<>
添加编译期限制,比如模板参数类型为整型时禁用拷贝构造:
template<typename T>
class C
{
public:
...
// user-define the predefined copy constructor as deleted
// (with conversion to volatile to enable better matches)
C(C const volatile&) = delete;
// if T is no integral type, provide copy constructor template with better match:
template<typename U,
typename = std::enable_if_t<!std::is_integral<U>::value>>
C (C<U> const&) {
...
}
...
};
使用 Concepts 简化 enable_if<> 表达式
使用 enable_if<>
使用起来比较繁琐,并且可读性比较差。C++20 引入 Concepts
可以大大简化 enable_if<>
表达式。例如:
template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}
也可以简化为:
template<typename T>
concept ConvertibleToString = std::is_convertible_v<T,std::string>;
template<typename STR>
requires ConvertibleToString<STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}
甚至可以简化为:
template<ConvertibleToString STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}
至此,本文结束。
参考:
- http://www.tmplbook.com