CPP-Templates-2nd--第六章 移动语义和 enable_if<>

目录

6.1 完美转发(Perfect Forwarding)

6.2 特殊成员函数模板

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

6.4 使用 enable_if<>

禁用某些成员函数

6.5 使用 concept 简化 enable_if<>表达式

 6.6 总结


 

参考:GitHub - Walton1128/CPP-Templates-2nd--: 《C++ Templates 第二版》中文翻译,和原书排版一致,第一部分(1至11章)以及第18,19,20,21、22、23、24、25章已完成,其余内容逐步更新中。 个人爱好,发现错误请指正

6.1 完美转发(Perfect Forwarding)

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

 可变对象被转发之后依然可变。

 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&)

}
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&&)
}

注意其中针对可移动对象(一个右值引用)的代码不同于其它两组代码;它需要用 std::move() 来处理其参数,因为参数的移动语义不会被一起传递。虽然第三个 f()中的 val 被声明成右值 引用,但是当其在 f()内部被使用时,它依然是一个非常量左值(参考附录 B),其行为也将 和第一个 f()中的情况一样。因此如果不使用 std::move()的话,在第三个 f()中调用的将是 g(X&) 而不是 g(X&&)。

如果试图在泛型代码中统一以上三种情况,会遇到这样一个问题:

template<typename T>
void f (T val) {
g(val);
}

这个模板只对前两种情况有效,对第三种用于可移动对象的情况无效。 基于这一原因,C++11 引入了特殊的规则对参数进行完美转发(perfect forwarding)。实现 这一目的的惯用方法如下:

template<typename T>
void f (T&& val) {
g(std::forward<T>(val)); // perfect forward val to g()
}

注意 std::move 没有模板参数,并且会无条件地移动其参数;而 std::forward<>会跟据被传递 参数的具体情况决定是否“转发”其潜在的移动语义。

一个可以完美转发其参数的程序会像下面这样:

#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&&)
}

6.2 特殊成员函数模板

#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 

	Person p4(std::move(p1)); // OK: move Person => calls MOVECONST
	Person const p2c("ctmp"); //init constant object with string literal
	Person p3c(p2c); // OK: copy constant Person => 
	Person p3(p1); // ERROR

 当试图调用拷贝构造函数的时候,会遇到错误: Person p3(p1); // ERROR

Function templates can be overloaded with nontemplate functions. All else being equal, the non-template function is preferred in selecting the actual function being called. 

函数模板可以用非模板函数重载。在其他条件相同的情况下,在选择要调用的实际函数时,首选非模板函数。

However, when const and reference qualifiers differ, priorities for overload resolution can change.

但是,当const和引用限定符不同时,重载解析的优先级可能会改变。

详细参见 16.2.4 Templates and Nontemplates

额外提供一个非 const 的拷贝构造函数看上去是个不错的方法: Person (Person& p)

不过这只是一个部分解决问题的方法,更好的办法依然是使用模板。

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

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

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

这一模板定义会在 sizeof(T) > 4 不成立的时候被忽略掉。如果 sizeof > 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<>,这样就可以省略掉 template和::type

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

由于将 enable_if 表达式放在声明的中间不是一个明智的做法,因此使用 std::enable_if<> 的更常见的方法是使用一个额外的、有默认值的模板参数:

template<typename T, typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}

如果你认为这依然不够明智,并且希望模板的约束更加明显,那么你可以用别名模板(alias template)给它定义一个别名:

template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
template<typename T, typename = EnableIfSizeGreater4<T>>
void foo() {
}

6.4 使用 enable_if<>

我们要解决的问题是:当传递的模板参数的类型不正确的时候(比如不是 std::string 或者可 以转换成 std::string 的类型),禁用如下构造函数模板:

template<typename STR>

Person(STR&& n)

需要使用另一个标准库的类型萃取,std::is_convertiable<FROM,TO>。

#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 

	Person p4(std::move(p1)); // OK: move Person => calls MOVECONST
	Person const p2c("ctmp"); //init constant object with string literal
	Person p3c(p2c); // OK: copy constant Person => 
	Person p3(p1); // ERROR
	return 0;
}

除 了 使 用 要 求 类 型 之 间 可 以 隐 式 转 换 的 std::is_convertible<> 之 外 , 还 可 以 使 用 std::is_constructible<>,它要求可以用显式转换来做初始化。但是需要注意的是,它的参数 顺序和 std::is_convertible<>相反:

禁用某些成员函数

注意我们不能通过使用 enable_if<>来禁用 copy/move 构造函数以及赋值构造函数。这是因 为成员函数模板不会被算作特殊成员函数(依然会生成默认构造函数),而且在需要使用 copy 构造函数的地方,相应的成员函数模板会被忽略掉。

因此即使像下面这样定义类模板:

class C {
public:
template<typename T>
C (T const&) {
std::cout << "tmpl copy constructor\n";} …
};

在需要 copy 构造函数的地方依然会使用预定义的 copy 构造函数:

6.5 使用 concept 简化 enable_if<>表达式

通过使用 concept 可以写出下面这样的代码:

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)) { …
}

 6.6 总结

 可以通过使用 std::enable_if<>并在其条件为 false 的时候禁用模板。

 通过使用 std::enable_if<>,可以避免一些由于构造函数模板或者赋值构造函数模板比隐 式产生的特殊构造函数更加匹配而带来的问题。

 通过 concept 可以使用更直观的语法对函数模板施加限制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值