【C++ Templates(6)】移动语义与enable_if
2018-04-16 本文已影响162人
downdemo
完美转发
- 假设想写出转发传递实参的如下基本属性的泛型代码
- 可修改的对象转发后应该仍可以被修改
- 常量对象应该作为只读对象转发
- 可移动的对象应该作为可移动对象转发
- 如果不使用模板实现这些功能,必须编写全部的三种情况,比如转发一个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&&)
}
- 如果想组合这三种情况,就会有一个问题,对前两种情况下面代码是适用的,但是对于可移动的对象则不适用
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()
}
- 特定类型的X&&和模板参数T&&适用不同的规则
- X&&声明一个右值引用,它只能绑定到一个可移动对象上(一个prvalue,比如临时对象,xvalue,以及用std::move()传递的对象),它总是可修改的,你总可以窃取它的值
- T&&声明一个转发引用,它能绑定到一个可修改的,不可修改的(即const)或可移动的对象上,在函数定义中,参数可能是可修改的,不可修改的,或一个你总能窃取的内部值
- 注意T必须是模板参数名称,仅仅依赖于模板参数是不够的。比如对模板参数T,typename 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&&)
}
特殊成员函数模板
- 成员函数模板也能用于特殊的成员函数,包括构造函数,但这可能导致意外的行为
- 下面是未使用模板的代码
#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
}
- 现在用泛型构造器完美转发实参给成员name,替代原有的两个string构造器
#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";
}
};
- 构造函数对于string将正常工作
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); // ERROR
- 由一个可移动对象初始化一个新的Person可以正常工作
Person p4(std::move(p1)); // OK: move Person => calls MOVECONST
- 拷贝一个常量的Person也可行
Person const p2c("ctmp"); //init constant object with string literal
Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
- 问题在于,对于非常量左值的重载解析,成员模板比拷贝构造函数更匹配
template<typename STR>
Person(STR&& n)
// 优于
Person (Person const& p)
- 一种解决方法是提供一个非常量的拷贝构造函数,但这只是一个局限的解决方案,因为对于派生类对象,成员模板仍然是更好的匹配。最好的方法是,传递实参为Person或一个能转换为Person的表达式时,禁用成员模板
Person (Person& p)
使用enable_if<>禁用成员模板
- C++11提供了std::enable_if在某个编译期条件下忽略函数模板
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
}
- 如果(sizeof(T) > 4)为false,foo<>()的定义将被忽略,若为true则函数模板实例扩展为
void foo() {
}
- 也就是说,std::enable_if<>是一个类型trait,它评估给定的编译期表达式,将其作它的首个模板实参传递,且表现如下
- 如果表达式为true,类型成员type产生一个类型
- 如果没有传递第二个实参,type为void
- 否则,type是第二个模板实参类型
- 如果表达式为false,type未定义,由于SFINAE,这个带有enable_if的函数模板将被忽略
- 如果表达式为true,类型成员type产生一个类型
- C++14中所有产生一个类型的类型trait,有一个对应的别名模板std::enable_if_t<>,它允许跳过typename和::type,因此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() {
}
- 如果sizeof(T)>4,扩展为
template<typename T,
typename = void>
void foo() {
}
- 如果还觉得笨拙,可以使用别名模板定义自己的名称
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
template<typename T,
typename = EnableIfSizeGreater4<T>>
void foo() {
}
使用enable_if<>
- 现在来解决之前的构造函数模板的问题,当实参STR有正确的类型(std::string或可以转换为std::string的类型)时禁用声明
template<typename STR>
Person(STR&& n);
- 为此,使用另一个标准类型trait,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类如下
// basics/specialmemtmpl3.hpp
#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中必须声明别名模板如下,因为产生一个值的类型trait的_v版本是未定义的
template<typename T>
using EnableIfString =
std::enable_if_t<std::is_convertible<T,std::string>::value>;
- 并且在C++11中,必须声明特殊的成员模板如下,因为产生一个值的类型trait的_t版本是未定义的
template<typename T>
using EnableIfString
= typename std::enable_if<std::is_convertible<T,std::string>::value>::type;
- 注意使用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)
- 同样不能删除预定义的拷贝构造函数,但有一个tricky方案,可以为const voilatile实参声明一个拷贝构造函数,并定义为=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<>添加额外的限制,比如如果模板参数类型是一个整型则禁止拷贝类模板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&) {
...
}
...
};
使用concept简化enable_if<>表达式
- 即使使用别名模板,enable_if语法也十分笨拙,concept允许用模板自己的语法制定要求和条件,但concept经过长时间讨论仍然还未成为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>;
- 再将这个concept制定为一个要求
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)) {
...
}