C++templates第六章移动语义及enable_if<>

移动语义(movesemantics)是 C++11 引入的一个重要特性。在 copy 或者赋值的时候,可以 通过它将源对象中的内部资源 move(“steal”)到目标对象,而不是 copy 这些内容。当然 这样做的前提是源对象不在需要这些内部资源或者状态(因为源对象将会被丢弃)。
移动语义对模板的设计有重要影响,在泛型代码中也引入了一些特殊的规则来支持移动语义。本章将会介绍移动语义这一特性。

完美转发

假设希望实现的泛型代码可以将被传递参数的基本特性转发出去:

  • 可变对象被转发之后依然可变
  • Const 对象被转发之后依然是 const 的
  • 可移动对象(可以从中窃取资源的对象)被转发之后依然是可移动的。

不适用模板的时候,达成这一目的需要对这三种情况分别编程,如下所示是将调用f()时传递的参数转发给g():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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 ::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()forconstantobjectcallsf(Xconst&)=>callsg(Xconst&)
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()函数:它需要用 std::move() 来处理其参数,因为参数的移动语义不会被一起传递。虽然第三个 f()中的 val 被声明成右值引用,但是当其在 f()内部被使用时,它依然是一个非常量左值(参考附录 B),其行为也将 和第一个f()中的情况一样。因此如果不使用std::move()的话, 在第三个f()中调用的将是g(X&) 而不是 g(X&&)。

如果尝试泛型代码统一以上三种情况,会出现以下问题:

1
2
3
4
5
template<typename T> 
void f (T val)
{
g(val);
}

这个模板只对前两种情况有效,对第三种用于可移动对象的情况无效。

基于这一原因,C++11 引入了特殊的规则对参数进行完美转发(perfectforwarding)。实现 这一目的的惯用方法如下:

1
2
3
4
5
6
template<typename T> 
void f (T&& val)
{
g(std::forward<T>(val)); // perfect forward val to g()
}

不要以为模板参数 T 的 T&&和具体类型 X 的 X&&是一样的。虽然语法上看上去类似,但是它们适用于不同的规则:

  • 具体类型 X 的 X&&声明了一个右值引用参数。只能被绑定到一个可移动对象上(一个 prvalue,比如临时对象,一个 xvalue,比如通过 std::move()传递的参数)。它的值总是可变的,而且总是可以被“窃取”。
  • 模板参数 T 的 T&&声明了一个转发引用(亦称万能引用)。可以被绑定到可变、不可变(比如 const)或者可移动对象上。在函数内部这个参数也可以是可变、不可变或者指向一个可以被窃取内部数据的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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() perfect forward argument val to g():
template<typename T>
void f (T&& val)
{
g(std::forward<T>(val)); // call the right g() for any passed argument val
}
int main()
{
X v; // create variable
X const c; // create constant
f(v); // f() for variable calls f(X&) => calls g(X&)
f(c); // f() for constant 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 move-enabled variable calls f(X&&)=> calls g(X&&)
}

通过 std::enable_if<>禁用模板

从 C++11 开始,通过 C++标准库提供的辅助模板 std::enable_if<>,可以在某些编译期条件下忽略掉函数模版

比如,如果函数模板 foo<>的定义如下:

1
2
template<typename T> 
typename std::enable_if<(sizeof(T) > 4)>::type foo() { }

也就是说 std::enable_if<>是一种类型萃取(typetrait),它会根据一个作为其(第一个)模板参数的编译期表达式决定其行为:

  • 如果这个表达式结果为 true,它的 type 成员会返回一个类型:
    • 如果没有第二个模板参数,返回类型是 void。
    • 否则,返回类型是其第二个参数的类型。
  • 如果表达式结果 false,则其成员类型是未定义的。根据模板的一个叫做 SFINAE (substitutefailureisnotanerror,替换失败不是错误,将在 8.4 节进行介绍)的规则, 这会导致包含 std::enable_if<>表达式的函数模板被忽略掉。

由于从 C++14 开始所有的模板萃取(typetraits)都返回一个类型,因此可以使用一个与之 对应的别名模板 std::enable_if_t<>,这样就可以省略掉 template 和::type 了。如下:

1
2
template<typename T> 
std::enable_if_t<(sizeof(T) > 4)> foo() { }