文章目录
- 一、完美转发(Perfect Forwarding)
- 二、特殊成员函数模板:构造函数
- 三、 通过 std::enable_if<>禁用模板
- 四、 使用 enable_if<>
- 1.不能通过使用 enable_if<>来禁用 copy/move 构造函数以及赋值构造函数
- 五、使用 concept 简化 enable_if<>表达式
一、完美转发(Perfect Forwarding)
- eg:要求:
可变对象被转发之后依然可变。
Const 对象被转发之后依然是 const 的。
可移动对象(可以从中窃取资源的对象) 被转发之后依然是可移动的
不使用模板的话:将调用 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&)
}
/*
它需要用 std::move()来处理其参数, 因为参数的移动语义不会被一起传递。
虽然第三个 f()中的 val 被声明成右值引用, 但是当其在 f()内部被使用时,
它依然是一个非常量左值 , 其行为也将和第一个 f()中的情况一样。
因此如果不使用 std::move()的话, 在第三个 f()中调用的将是 g(X&)而不是 g(X&&)。
*/
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() 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&&)
}
- eg1:第一种模板写法
缺点:这个模板只对前两种情况有效, 对第三种用于可移动对象的情况无效。
template<typename T>
void f (T val) {
g(val);
}
- eg2:对参数进行完美转发(perfect forwarding) 的惯用手法
注意 std::move 没有模板参数, 并且会无条件地移动其参数;
而 std::forward<>会跟据被传递参数的具体情况决定是否“转发” 其潜在的移动语义。
template<typename T>
void f (T&& val) {
g(std::forward<T>(val)); // perfect forward val to g()
}
- 注意:模板参数 T 的 T&&和具体类型 X 的 X&&不是一样的。
具体类型 X 的 X&&声明了一个右值引用参数。 只能被绑定到一个可移动对象上( 一个rvalue, 比如临时对象, 一个 xvalue, 比如通过 std::move()传递的参数,
模板参数 T 的 T&&声明了一个转发引用( 亦称万能引用) 。
可以被绑定到可变、 不可变(比如 const) 或者可移动对象上。
在函数内部这个参数也可以是可变、 不可变或者指向一个可以被窃取内部数据的值
- eg3:完美转发其参数的程序
#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&&)
}
二、特殊成员函数模板:构造函数
- eg:考虑下面这个例子:
#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
}
- eg:泛型的构造函数
#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";
}
};
int main()
{
std::string s = "sname";
Person p1(s); // init with string object => calls TMPL-CONSTR
/*
注意这里在构建 p2 的时候并不会创建一个临时的 std::string 对象: STR 的类型被推断为 char const[4]。
但是将 std::forward<STR>用于指针参数没有太大意义。 成员 name 将会被一个以null 结尾的字符串构造。
*/
Person p2("tmp"); //init with string literal => calls TMPL-CONS
//当试图调用拷贝构造函数的时候, 会遇到错误:
/*
对于一个非 const 左值的 Person p,
成员模板
template<typename STR>
Person(STR&& n)
通常比预定义的拷贝构造函数更匹配:
Person (Person const& p)
*/
Person p3(p1); // ERROR
//而用一个可移动对象初始化 Person 的话却可以正常工作:
Person p4(std::move(p1)); // OK: move Person => calls MOVECONST
//如果试图拷贝一个 Person 的 const 对象的话, 也没有问题:
Person const p2c("ctmp"); //init constant object with string literal
Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
}
解决办法:
- 额外提供一个非 const 的拷贝构造函数看上去是个不错的方法:Person (Person& p)
- 更好的办法依然是使用模板。 我们真正想做的是当参数是一个 Person 对象或者一个可以转换成 Person 对象的表达式时, 不要启用模板
三、 通过 std::enable_if<>禁用模板
辅助模板 std::enable_if<>, 可以在某些编译期条件下忽略掉函数模板。
- eg:这一模板定义会在 sizeof(T) > 4 不成立的时候被忽略掉。
//如果函数模板 foo<>的定义如下:
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {}
//如果 sizeof<T> > 4 成立, 函数模板会展开成:
template<typename T>
void foo() {
}
也就是说 std::enable_if<>是一种类型萃取(type trait) , 它会根据一个作为其(第一个) 模板参数的编译期表达式决定其行为:
如果这个表达式结果为 true, 它的 type 成员会返回一个类型:
- 如果没有第二个模板参数, 返回类型是 void。
- 否则, 返回类型是其第二个参数的类型。
如果表达式结果 false, 则其成员类型是未定义的。 根据模板的一个叫做 SFINAE
(substitute failure is not an error, 替换失败不是错误) 的规则,
这会导致包含 std::enable_if<>表达式的函数模板被忽略掉。
C++14 开始所有的模板萃取( type traits) 都返回一个类型, 因此可以使用一个与之对应的别名模板 std::enable_if_t<>, 这样就可以省略掉 typename和::type 了
- eg:
template<typename T>
std::enable_if_t<(sizeof(T) > 4)>
foo() {
}
- eg:那么在 sizeof(T) > 4 时, enable_if 会被扩展成其第二个模板参数。
template<typename T>
std::enable_if_t<(sizeof(T) > 4), T>
foo() {
return T();
}
//此如果与 T 对应的模板参数被推断为 MyType, 而且其 size 大于 4, 那么其等效于:
MyType foo();
- eg:使用 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<>
使用 enable_if<>可以解决构造函数模板的问题。
- 我们要解决的问题是: 当传递的模板参数的类型不正确的时候(比如不是std::string 或者可以转换成 std::string 的类型) , 禁用如下构造函数模板:
template<typename STR>
Person(STR&& n);
- 构造函数模板的定义如下:std::is_convertiable<FROM, TO>。
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);
//这里同样可以使用别名模板给限制条件定义一个别名:
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;
使 用 要 求 类 型 之 间 可 以 隐 式 转 换 的 std::is_convertible<> 之 外 , 还 可 以 使 用std::is_constructible<>, 它要求可以用显式转换来做初始化。
- 但是需要注意的是, 它的参数顺序和 std::is_convertible<>相反:
template<typename T>
using EnableIfString =
std::enable_if_t<std::is_constructible_v<std::string, T>>;
1.不能通过使用 enable_if<>来禁用 copy/move 构造函数以及赋值构造函数
这是因为成员函数模板不会被算作特殊成员函数( 依然会生成默认构造函数) , 而且在需要使用copy 构造函数的地方, 相应的成员函数模板会被忽略掉。
- eg:
class C {
public:
template<typename T>
C (T const&) {
std::cout << "tmpl copy constructor\n";}
…
};
//在需要 copy 构造函数的地方依然会使用预定义(编译器产生的)的 copy 构造函数
C x;
C y{x}; // still uses the predefined copy constructor (not the member template)
- 解决办法:可以定义一个接受 const volatile 的 copy 构造函数并将其标示为 delete。
这样做就不会再隐式声明一个接受 const 参数的 copy 构造函数。
在此基础上, 可以定义一个构造函数模板, 对于 nonvolatile 的类型, 它会优先被选择(相较于已删除的 copy 构造函数) :
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";
}
…
};
//这样即使对常规 copy, 也会调用模板构造函数:
C x;
C y{x}; // uses the member template
- eg:还可以禁止对通过 int 类型参数实例化出来的 C<>模板实例进行 copy:
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, 可以通过其简单的语法对函数模板施加限制条件。
- eg:通过使用 concept 可以写出下面这样的代码: