C++模板(第二版)笔记之第六章:移动语义和 enable_if

news2024/10/7 20:30:23

文章目录

  • 一、完美转发(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 可以写出下面这样的代码:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/86107.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

0124 双指针 Day13

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面 输入一个整数数组&#xff0c;实现一个函数来调整该数组中数字的顺序&#xff0c;使得所有奇数在数组的前半部分&#xff0c;所有偶数在数组的后半部分。 示例&#xff1a; 输入&#xff1a;nums [1,2,3,4] 输出&#xff1a…

net基于asp.net的计算机网络课程题库系统-计算机毕业设计

项目介绍 计算机网络课程题库系统是针对目前计算机网络课程试题的实际需求,从实际工作出发,对过去的计算机网络课程试题平台存在的问题进行分析,完善用户的使用体会。采用计算机系统来管理信息,取代人工管理模式,查询便利,信息准确率高,节省了开支,提高了工作的效率。 本系统结…

操作系统_线程安全问题

文章目录1.线程安全问题举例2.为什么会有线程安全问题3.如何解决线程安全问题1.从原子性入手解决线程安全问题2.synchronized的使用方法3.java标准库中的线程安全类4.死锁问题举例2.死锁的必要条件1.线程安全问题举例 看代码: class Count {int i 0;public void add(){i;} }…

Sklearn机器学习与Plotly可视化强强联合

在学习sklearn(机器学习)过程中&#xff0c;模型原理可谓是枯燥无味&#xff0c;加上大多数模型训练过程也是不可见的&#xff0c;这使得很多小伙伴们望而却步&#xff0c;当然也有很多学者试图通过各种方式以可视化模型学习及预测过程&#xff0c;但大多数是复杂且不美观的。 …

Windows后台运行并启动Frpc客户端界面

Windows后台运行并启动Frpc客户端界面 frp搭建内网穿透可以看我另外一篇 1.frps服务端配置 [common] bind_port 3000 vhost_http_port 4000 vhost_https_port 5000authentication_method token authenticate_new_work_conns true token 5ae9394f-32d8-4a58-b6ed-e9f36…

【微服务技术05】Ribbon负载均衡

【微服务技术05】Ribbon负载均衡 案例代码&#xff1a;https://gitee.com/pikachu2333/spring-cloud-hexuan 之前配置好了eureka注册中心&#xff0c;使用RestTemplate调用地址为&#xff1a;http://eureka-user-service/user/1&#xff0c;配置了LoadBalanced负载均衡注解 但…

使用RTP包荷载AAC码流数据

目录 一. 前言 二. RTP协议介绍 三. AAC介绍 1. AAC格式 2. ADTS 四. RTP与AAC的结合 五. 代码实战 六. 效果展示 一. 前言 音视频通话中我们通常使用 RTP 协议包荷载音视频码率数据&#xff0c;例如麦克风采集输入数据后编码成帧&#xff0c;再将帧数据放入 RTP 协议包…

B站:以SLO为核心的可用性观测与质量运营

UGeek大咖说是优维科技为技术爱好者研讨云原生技术演进趋势而创办的系列活动&#xff0c;邀请一线互联网大厂的核心骨干主讲&#xff0c;分享原厂实践。本年度主题为可观测&#xff0c;我们希望通过一场场有趣、有料、有深度的活动&#xff0c;让运维圈的小伙伴聚集在一起&…

Java知识点--反射(上)

Java知识点--反射&#xff08;上&#xff09;&#x1f356;一、为什么需要反射1️⃣在特定情境中传统方法的不足2️⃣为了不修改原码引出反射&#x1f357;二、反射机制1️⃣Java反射机制2️⃣Java 反射机制原理示意图3️⃣Java 反射机制可以完成4️⃣反射相关的主要类5️⃣反射…

java计算机毕业设计ssm在线学习资源管理系统t4ko5(附源码、数据库)

java计算机毕业设计ssm在线学习资源管理系统t4ko5&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java丹徒高级中学校车预约9poqj

大四计算机专业的同学们即将面临大学4年的最后一次考验--毕业设计。通过完成毕业设计来对过去4年的大学学习生活做一个总结&#xff0c;也是检验我们学习成果的一种方式&#xff0c;毕业设计作品也是我们将来面试找工作的一个敲门砖。 选题前先看看自己掌握哪些技术点、擅长哪…

Denoising Diffusion Probabilistic Models

目录概要前向过程nice property逆向过程参数推导简化参考资料概要 Denoising Diffusion Probabilistic Model(DDPM)是一个生成模型&#xff0c;给定一个目标分布&#xff0c;学习模型以便可以从目标分布中采样。 使用马尔科夫链建模。输入是噪声&#xff0c;通过神经网络逐步去…

伟大的缝纫师—typedef

伟大的缝纫师—typedef一.历史的误会—也许应该是typerename二.typedef和#define的区别一.历史的误会—也许应该是typerename 为什么这样说呢&#xff1f;因为typedf其实就是一个重命名关键字&#xff0c;看示例 这里我觉得unsigned int太长了&#xff0c;我将它改名为u_int&am…

bat批处理文件的注释,和常用简单命令

参考&#xff1a;https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/windows-commands&#xff0c;https://blog.csdn.net/wuhenyouyuyouyu/article/details/120736519? 当前路径&#xff1a;%~dp0 这个参数只能在bat文件中在正常使用&…

Unity脚本(2) --- 脚本生命周期以及脚本的控制台调试

1.什么是脚本生命周期&#xff1f; 首先什么是脚本 --- 脚本的本质其实就是类&#xff0c;而脚本生命周期其实就是脚本对应的那个类从开始工作到最后销毁这么一个周期 &#xff08;或者说是Unity脚本从唤醒到销毁的过程&#xff09; &#xff08;消息&#xff0c;必然事件&am…

[附源码]Node.js计算机毕业设计儿童成长记录与分享系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

【Vue实践】尚硅谷张天禹Vue学习笔记(更新至第86课)-20221126~20221212

004_尚硅谷Vue技术_搭建Vue开发环境 搭建Vue.js devtools 允许访问文件网址 https://blog.csdn.net/sunhl951/article/details/80185628 阻止 vue 在启动时生成生产提示。 Vue.config.productionTip false 目测没有用 https://blog.csdn.net/DIUDIUjiang/article/details/…

这些车企在企业微信里,装上高速的“组织引擎”

“这真是一场惊险之旅。” 今年7月&#xff0c;胡先生一家疾驶在若羌县罗布泊镇国道上&#xff0c;迎面突然冲出一辆大型货车……为了避让&#xff0c;胡太太驾驶的极氪001撞上了路边的石墩&#xff0c;两个轮胎直接报废。 在人迹罕至的无人区&#xff0c;保险公司鞭长莫及&a…

C++ 基础篇之如何进行数据封装

&#x1f4d2;博客主页&#xff1a; ​​开心档博客主页​​ &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐留言&#x1f4dd; &#x1f4cc;本文由开心档原创&#xff01; &#x1f4c6;51CTO首发时间&#xff1a;&#x1f334;2022年12月12日&#x1f334; ✉…

rocketmq源码-producer启动流程

前言 DefaultMQProducer producer new DefaultMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("127.0.0.1:9876");producer.setNamesrvAddr("127.0.0.1:9876");producer.start();创建、启动producer的逻辑&#xff…