C ++ 11引入的最突出的功能之一是移动语义。 通过将内部资源从源对象移动(“窃取”)到目标对象,而不用复制这些内容,可以使用它来优化复制和分配。 如果源不再需要其内部值或状态(因为它将被丢弃),则可以完成此操作。 移动语义对模板的设计有重大影响,并且引入了特殊规则来支持通用代码中的移动语义。 本章介绍这些功能。
6.1 完美转发
假设您要编写通用代码来转发传递的参数的基本属性:
•应转发可修改的对象,以便仍可以对其进行修改。
•常量对象应作为只读对象转发。
•可移动对象(由于它们即将过期而可以“窃取”的对象)应作为可移动对象转发。
为了在没有模板的情况下实现此功能,我们必须对所有三种情况进行编程。 例如,要将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()的三种不同实现,将其参数转发给g():
// 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\&\&)
}
请注意,用于可移动对象的代码(通过右值引用)与其他代码不同:它需要std::move(),因为根据语言规则,移动语义不会通过①。 尽管第三个f()中的val被声明为右值引用,但在用作表达式时,其值类别是非恒定的左值(请参阅附录B),并且在第一个f()中的行为类似于val。 如果没有move(),则将调用非恒定左值的g(X&)而不是g(&&)。
如果要在通用代码中结合所有这三种情况,则会遇到问题:
template<typename T>
void f (T val) {
g(T);
}
适用于前两种情况,但不适用于传递可移动对象的(第三种)情况。
因此,C ++ 11引入了用于完善转发参数的特殊规则。 实现此目的的惯用代码模式如下:
template<typename T>
void f (T&& val) {
g(std::forward<T>(val)); // perfect forward val to g()
}
请注意,std::move()没有模板参数,传递的参数“触发”了移动语义,而std::forward <>()根据传递的模板参数“转发(forward)”了潜在的移动语义。
请勿假设模板参数T的T &&行为与特定类型X的X &&行为相同。适用不同的规则!但是,从语法上看,它们看起来相同:
•对于特定类型X的X &&声明参数为右值引用。它只能绑定到可移动对象(prvalue(例如临时对象)和xvalue(例如通过std :: move()传递的对象);有关详细信息,请参见附录B)。它总是可变的,您总是可以“窃取”它的价值。②
•模板参数T的T &&声明转发引用(也称为通用引用)。③ 它可以绑定到可变的,不可变的(即const)或可移动对象。在函数定义内部,该参数可能是可变的,不可变的,或指向您可以“窃取”内部信息的值。
注意,T必须确实是模板参数的名称。仅依靠模板参数是不够的。对于模板参数T,诸如类型名T::iterator &&之类的声明只是一个右值引用,而不是转发引用。
因此,用于完善前向自变量的整个程序将如下所示:
#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\&\&)
}
当然,完美的转发也可以与可变参数模板一起使用(有关某些示例,请参见第60页的第4.3节)。 有关完美转发的详细信息,请参见第280页的15.6.3节。
6.2 特殊成员函数模板
成员函数模板还可以用作特殊的成员函数,包括用作构造函数,但是,这可能会导致令人惊讶的行为。
考虑以下示例:
#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
}
在这里,我们有一个Person类,具有一个字符串成员名称,为此我们提供了初始化构造函数。 为了支持移动语义,我们使用std::string重载了构造函数:
•我们为调用者仍需要的字符串对象提供了一个版本,其名称由传递的参数的副本初始化:
Person(std::string const& n) : name(n) {
std::cout << "copying string-CONSTR for ’" << name << "’\n";
}
•我们为可移动字符串对象提供了一个版本,为此我们调用std::move()来“窃取”来自以下内容的值:
Person(std::string&& n) : name(std::move(n)) {
std::cout << "moving string-CONSTR for ’" << name << "’\n";
}
如预期的那样,第一个调用用于正在使用的传递的字符串对象(左值),而第二个调用用于可移动对象(右值):
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
除了这些构造函数之外,该示例还为copy和move构造函数提供了特定的实现,以查看将Person整体复制/移动的时间:
Person p3(p1); // copy Person => calls COPY-CONSTR
Person p4(std::move(p1)); // move Person => calls MOVE-CONSTR
现在,让我们用一个通用构造函数替换两个字符串构造函数,以完美地将传递的参数转发给成员名称:
#include <utility>
#include <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";
}
};
如预期的那样,传递字符串的构造工作正常:
std::string s = "sname";
Person p1(s); // init with string object => calls TMPL-CONSTR
Person p2("tmp"); //init with string literal => calls TMPL-CONSTR
请注意,在这种情况下p2的构造如何不会创建临时字符串:推导参数STR为char const [4]类型。 将std::forward <STR>应用于构造函数的指针参数没有太大作用,因此name成员是由一个以null终止的字符串构造的。
但是,当我们尝试调用复制构造函数时,会出现错误:
Person p3(p1); // ERROR
虽然通过可移动对象初始化新的Person仍然可以正常工作:
Person p4(std::move(p1)); // OK: move Person => calls MOVE-CONST
请注意,复制常量Person也可以正常工作:
Person const p2c("ctmp"); //init constant object with string literal
Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
问题是,根据C ++的重载解析规则(请参阅第333页的16.2.4节),对于非恒定左值Person p成员模板
template<typename STR>
Person(STR&& n)
比(通常是预定义的)副本构造函数更好地匹配:
Person (Person const& p)
STR只是用Person&代替,而对于复制构造函数,必须转换为const。
您可能会考虑通过提供非恒定拷贝构造函数来解决此问题:
Person (Person& p)
但是,这只是部分解决方案,因为对于派生类的对象,成员模板仍然是更好的匹配。 您真正想要的是在传递的参数是Person或可以转换为Person的表达式的情况下禁用成员模板。 这可以通过使用std :enable_if <>完成,这将在下一部分中介绍。
6.3 用enable_if<>禁用模板
从C ++ 11开始,C ++标准库提供了一个帮助程序模板std::enable_if <>在某些编译时条件下忽略函数模板。
例如,如果函数模板foo <>()定义如下:
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
}
如果sizeof(T)> 4得出false,则忽略foo <>()的定义。④ 如果sizeof(T)> 4得出true,函数模板实例扩展为
void foo() {
}
也就是说,std::enable_if <>是一种类型特征,它评估作为其(第一个)模板参数传递的给定编译时表达式,其行为如下:
•如果表达式的结果为true,则其类型成员类型的结果为类型:
–如果未传递第二个模板参数,则该类型为void。
–否则,该类型是第二个模板参数类型。
•如果表达式的结果为false,则未定义成员类型。 由于稍后介绍的名为SFINAE(替代/演绎失败不是错误)的模板功能(请参阅第129页的8.4节),其作用是忽略带有enable_if表达式的功能模板。
至于自C ++ 14起所有产生类型的类型特征,都有一个对应的别名模板std :: enable_if_t <>,该模板允许您跳过typename和:: type(有关详细信息,请参见第40页的2.8节)。
因此,由于C ++ 14,您可以编写
template<typename T>
std::enable_if_t<(sizeof(T) > 4)>
foo() {
}
如果将第二个参数传递给enable_if <>或enable_if_t <>:
template<typename T>
std::enable_if_t<(sizeof(T) > 4), T>
foo() {
return T();
}
如果表达式产生true,则enable_if构造扩展为第二个参数。 因此,如果MyType是传递或推导为T的具体类型,其大小大于4,则效果为
MyType foo();
请注意,在声明的中间包含enable_if表达式非常笨拙。 因此,使用std :: enable_if <>的常用方法是使用具有默认值的附加函数模板参数:⑤
template<typename T,
typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}
扩展成
template<typename T, typename = void>
void foo() {
}
如果sizeof(T)> 4。
如果那仍然太笨拙,并且您想使需求/约束更加明确,则可以使用别名模板为其定义自己的名称:
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
template<typename T, typename = EnableIfSizeGreater4<T>>
void foo() {
}
有关如何实现std :: enable_if的讨论,请参见第469页的20.3节。
6.4 使用enable_if<>
我们可以使用enable_if <>通过第95页的6.2节中介绍的构造函数模板来解决我们的问题。
我们要解决的问题是禁用template constructor的声明
template<typename STR>
Person(STR&& n);
如果传递的参数STR具有正确的类型(即std::string或可转换为std::string的类型)。
为此,我们使用另一个标准类型特征,std :: is_convertible<FROM, TO>。 在C++17中,相应的声明如下所示:
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);
如果STR类型不能转换为std :: string类型,则将忽略整个函数模板。⑥
同样,我们可以使用别名模板为约束定义自己的名称:
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);
因此,整个类Person应该看起来如下:
#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";
}
};
现在,所有的调用行为均符合预期:
#include "specialmemtmpl3.hpp"
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
}
再次注意,在C ++ 14中,我们必须声明别名模板,如下所示,因为_v版本未针对产生值的类型特征定义:
template<typename T>
using EnableIfString = std::enable_if_t<std::is_convertible<T, std::string>::value>;
在C ++ 11中,我们必须声明特殊成员模板,如下所示,因为在编写时,_t版本未针对产生类型的类型特征定义:
template<typename T>
using EnableIfString = typename std::enable_if<std::is_convertible<T, std::string>::value>::type;
但这一切现在都隐藏在EnableIfString<>的定义中。
还请注意,还有一种使用std::is_convertible<>的替代方法,因为它要求类型可以隐式转换。 通过使用std::is_constructible<>,我们还允许将显式转换用于初始化。 但是,在这种情况下,参数的顺序相反:
有关std :: is_constructible <>的详细信息,请参见第719页的D.3.2节;有关std :: is_convertible <>的详细信息,请参见第727页的D.3.3节。 有关在可变参数模板上应用enable_if <>的详细信息和示例,请参见第734页的D.6节。
禁用特殊成员函数
请注意,通常我们不能使用enable_if <>禁用预定义的复制/移动构造函数和/或赋值运算符。 原因是成员函数模板从不算作特殊成员函数,例如在需要复制构造函数时将被忽略。 因此,使用此声明:
class C {
public:
template<typename T>
C (T const&) {
std::cout << "tmpl copy constructor\n";
}
...
};
当请求C的副本时,仍使用预定义的副本构造函数:
C x;
C y{x}; // still uses the predefined copy constructor (not the member template)
(实际上,没有方法使用成员模板,因为没有办法指定或推导其模板参数T。)
删除预定义的副本构造函数不是解决方案,因为尝试复制C会导致错误。
但是,有一个“刁钻”的解决方案:⑦ 我们可以为const volatile参数声明一个副本构造函数,并将其标记为“已删除”(即,使用= delete对其进行定义)。 这样做可以防止隐式声明另一个副本构造函数。 有了它,我们可以定义一个构造函数模板,对于non-volatile类型,该模板将比(已删除的)复制构造函数更“优先(preferred)”:
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参数是整数类型,则为了防止能够复制类模板C <>的对象,我们可以实现以下内容:
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&) {
...
}
...
};
6.5 使用concept简化enable_if 表达式
即使使用别名模板,enable_if语法也很笨拙,因为它使用了一种变通方法:为了获得理想的效果,我们添加了一个额外的模板参数,并“滥用”该参数以提供使函数模板完全可用的特定要求。像这样的代码很难阅读,并使其余功能模板难以理解。
原则上,我们只需要一种语言功能,即允许我们以某种方式制定功能的要求或约束,如果不满足要求/约束,则导致该功能被忽略。
这是期待已久的语言功能概念的应用,它使我们能够使用其自己的简单语法来制定模板的要求/条件。不幸的是,尽管经过了长时间的讨论,概念仍然没有成为C ++ 17标准的一部分。一些编译器为这种功能提供了实验性支持,但是,概念很可能成为C ++ 17之后的下一个标准的一部分。
对于概念,如建议使用它们,我们只需编写以下内容:
template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n): name(std::forward<STR>(n)) {
...
}
我们甚至可以将需求指定为普通concept
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)) {
...
}
有关C ++概念的详细讨论,请参见附录E。
6.6 Summary
•在模板中,您可以通过将它们声明为转发引用(声明为带有模板参数名称后跟&&的类型声明)并在转发调用中使用std::forward<>()来“完美”转发参数。
•使用完善的转发成员函数模板时,它们可能比预定义的特殊成员函数更好地匹配以复制或移动对象。
•使用std :: enable_if <>,可以在编译时条件为false时禁用功能模板(确定该条件后,该模板将被忽略)。
•通过使用std :: enable_if <>可以避免出现问题,因为可以为单个参数调用的构造函数模板或赋值运算符模板比隐式生成的特殊成员函数更好地匹配。
•您可以通过删除const volatile的预定义特殊成员函数来对特殊成员函数进行模板化(并应用enable_if <>)。
•概念将使我们能够对功能模板的要求使用更直观的语法。
注解:
①移动语义不会自动传递的事实是有意且重要的。如果不是这样,那么当我们在函数中首次使用可移动对象时,它将失去它的值。
②像X const &&这样的类型是有效的,但实际上并没有提供通用的语义,因为“窃取”可移动对象的内部表示需要修改该对象。但是,可以使用它来强制仅传递临时对象或标有std :: move()的对象,而不能对其进行修改。
③通用引用一词是斯科特·迈耶斯(Scott Meyers)创造的通用术语,可能会导致“左值引用”或“右值引用”。由于“通用”太普遍了,因此C ++ 17标准引入了术语转发参考,因为使用这种参考的主要原因是转发对象。但是,请注意它不会自动转发。期限
没有描述它是什么,而是描述它通常用于什么。
④不要忘记将条件放在括号中,否则条件中的>将会结束模板参数列表。
⑤感谢Stephen C. Dewhurst指出了这一点。
⑥如果您想知道为什么我们不检查STR是否“不能转换为Person”,请当心:我们正在定义一个函数,该函数可能允许我们将字符串转换为Person。因此,构造函数必须知道是否已启用它,这取决于它是否可转换,取决于它是否已启用,依此类推。切勿在会影响enable_if使用条件的地方使用enable_if。这是编译器未必检测到的逻辑错误。
⑦感谢Peter Dimov指出了该技术。