Modern Effective C++ 条款二十七:熟悉通用引用重载的替代方法

item26中说明对使用通用引用形参的函数,无论是独立函数还是成员函数,进行重载都会导致一系列问题。但是也提供了一些示例,如果能够按照我们期望的方式运行,重载可能也是有用的。这个条款探讨了几种通过避免在通用引用上重载的设计,或者通过限制通用引用可以匹配的参数类型,来实现所期望行为的方法。

放弃重载

为了解决这个问题,一个方法是完全避免重载。例如,如果你有两个不同行为的logAndAdd函数,你可以给它们不同的名字,如logAndAddName和logAndAddNameIdx。这种方法简单明了,但并不总是可行,特别是对于构造函数这样的情况,因为构造函数的名字是由类名决定的,不能随意更改。

传递const T&

另一种方法是使用const T&来代替通用引用。这种方式虽然可能没有通用引用那么高效,因为它不允许移动语义,但它可以确保更可预测的行为。例如,你可以将接受std::string的构造函数改为接受const std::string&,这样就不会与整数类型混淆了。

传值

还有一种方法是按值传递参数,这通常看起来像是降低了效率,但实际上可以通过移动语义优化性能。当你知道你将要拷贝对象时,直接传递值可以让编译器利用RVO(返回值优化)或NRVO(命名返回值优化),甚至是在某些情况下应用移动语义。

class Person {
public:// 使用std::string按值传递explicit Person(std::string n): name(std::move(n)) {}// 整数索引构造函数保持不变explicit Person(int idx): name(nameFromIdx(idx)) {}
private:static std::string nameFromIdx(int idx) {// 实现从索引获取名字的逻辑return "Name" + std::to_string(idx);}std::string name;
};

第一个构造函数接受std::string类型的参数,并使用std::move来转移所有权,允许编译器进行潜在的优化。第二个构造函数保持不变,接受整数作为参数并调用nameFromIdx函数来生成名字。由于std::string构造函数不会接受整型参数,所以这两个构造函数之间不会有冲突。如果用户尝试用整数初始化Person对象,那么将会调用正确的构造函数。对于std::string或者能够隐式转换为std::string的类型,将使用第一个构造函数。这样做既保证了代码的行为符合预期,又保持了良好的性能。

Tag Dispatch

Tag dispatch通过向函数传递一个额外的类型参数来帮助编译器选择正确的重载版本。这个类型参数通常是一个std::true_type或std::false_type,是标准库提供的类型,用于表示布尔值。这些类型在编译时已知,因此可以帮助编译器做出正确的决策。

logAndAdd 函数

有一个logAndAdd函数,能够处理两种情况:

当传入的是字符串或者可以转换为字符串的类型时,将该名字添加到全局数据结构。当传入的是整数时,使用这个整数作为索引来查找对应的名字,然后调用logAndAdd。

首先,需要定义两个实现函数logAndAddImpl,一个处理非整型,另一个处理整型。

//非整型实参:添加到全局数据结构中
template<typename T>
void logAndAddImpl(T&& name, std::false_type) {auto now = std::chrono::system_clock::now();log(now, "logAndAdd");names.emplace(std::forward<T>(name));
}
//整型实参:查找名字并用它调用logAndAdd
void logAndAddImpl(int idx, std::true_type) {logAndAdd(nameFromIdx(idx));
}
//辅助函数,从索引获取名字
std::string nameFromIdx(int idx) {
//实现从索引获取名字的逻辑return "Name" + std::to_string(idx);
}

接下来,编写主函数logAndAdd,根据传入的类型选择正确的logAndAddImpl版本:

template<typename T>
void logAndAdd(T&& name) {using UnrefType = typename std::remove_reference<T>::type;logAndAddImpl(std::forward<T>(name),std::is_integral<UnrefType>());
}

这里的关键点在于std::remove_reference<T>::type。当T是左值引用时(如int&),std::is_integral<int&>()会返回false,因为引用不是整型。因此我们需要使用std::remove_reference来移除引用,从而正确地判断T是否为整型。

//全局数据结构
std::multiset<std::string> names;
//日志记录函数
void log(const std::chrono::system_clock::time_point&, const char*) {//实现日志记录逻辑
}
//根据索引获取名字
std::string nameFromIdx(int idx) {//实现从索引获取名字的逻辑return "Name" + std::to_string(idx);
}
//非整型实参:添加到全局数据结构中
template<typename T>
void logAndAddImpl(T&& name, std::false_type) {auto now = std::chrono::system_clock::now();log(now, "logAndAdd");names.emplace(std::forward<T>(name));
}
//整型实参:查找名字并用它调用logAndAdd
void logAndAddImpl(int idx, std::true_type) {logAndAdd(nameFromIdx(idx));
}
//主函数,根据传入类型选择正确的logAndAddImpl版本
template<typename T>
void logAndAdd(T&& name) {using UnrefType = typename std::remove_reference<T>::type;logAndAddImpl(std::forward<T>(name),std::is_integral<UnrefType>());
}
int main() {logAndAdd("Alice");   // 添加字符串logAndAdd(42);        // 通过索引查找名字return 0;
}

通过这种方式,可以避免在通用引用上重载带来的问题,同时还能保持代码的简洁性和可读性。在C++中,当使用通用引用(T&&)时,尤其是与重载结合时,可能会导致一些意外的行为。为了解决这些问题,可以采用tag dispatch和std::enable_if来控制模板的启用条件。

std::enable_if 的基本形式

template<bool B, class T = void>
struct enable_if;
// 当 B 为 true 时,enable_if<B, T>::type 为 T
// 当 B 为 false 时,enable_if<B, T> 没有 type 成员
// 辅助函数,从索引获取名字
std::string nameFromIdx(int idx) {// 实现从索引获取名字的逻辑return "Name" + std::to_string(idx);
}
class Person {
public:// 完美转发构造函数,仅当T不是Person或其派生类且不是整型时启用template<typename T,typename = std::enable_if_t<!std::is_base_of<Person, std::decay_t<T>>::value&&!std::is_integral<std::remove_reference_t<T>>::value>>explicit Person(T&& n):name(std::forward<T>(n)) {}// 整型实参的构造函数explicit Person(int idx): name(nameFromIdx(idx)) {}// 拷贝构造函数Person(const Person& other): name(other.name) {}// 移动构造函数Person(Person&& other) noexcept: name(std::move(other.name)) {}
private:std::string name;
};
// 派生类示例
class SpecialPerson : public Person {
public://拷贝构造函数SpecialPerson(const SpecialPerson& rhs): Person(rhs) {}//移动构造函数SpecialPerson(SpecialPerson&& rhs) noexcept: Person(std::move(rhs)) {}
};

完美转发构造函数:

使用std::enable_if来限制模板的启用条件。

!std::is_base_of<Person, std::decay_t<T>>::value 确保T不是Person或其派生类。!std::is_integral<std::remove_reference_t<T>>::value 确保T不是整型。std::decay_t<T> 用于移除T的引用和cv限定符。

std::remove_reference_t<T> 用于移除T的引用。

整型实参的构造函数:直接处理整型参数,调用nameFromIdx函数来获取名字。

拷贝和移动构造函数:显式定义了拷贝和移动构造函数,确保它们不会被完美转发构造函数覆盖。

派生类:SpecialPerson 类继承自 Person,并显式定义了拷贝和移动构造函数,确保它们调用基类的相应构造函数。

std::enable_if 的基本形式如下:

template<bool B, class T = void>
struct enable_if;
// 当 B 为 true 时,enable_if<B, T>::type 为 T
// 当 B 为 false 时,enable_if<B, T> 没有 type 成员

在模板声明中,std::enable_if 通常用于 SFINAE(Substitution Failure Is Not An Error)规则,即如果模板实例化失败,则该模板不会被考虑为候选函数。

假设有一个 Person 类,有一个接受通用引用的构造函数,并且希望这个构造函数只对非 Person 类型及其派生类和非整型参数启用。使用 std::enable_if 来确保只有当传入的类型不是 Person 或其派生类,并且不是整型时,才启用该构造函数。

定义处理整型参数的构造函数:提供一个专门处理整型参数的构造函数。

使用 std::is_base_of 和 std::is_integral,std::is_base_of 用于检查类型是否是 Person 或其派生类。

std::is_integral 用于检查类型是否是整型。

使用std::decay 用于移除引用和 cv 限定符,确保类型比较时忽略这些修饰。

请记住:

  • 通用引用和重载的组合替代方案包括使用不同的函数名,通过lvalue-reference-to-const传递形参,按值传递形参,使用tag dispatch
  • 通过std::enable_if约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。
  • 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。

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

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

相关文章

【RL Application】语义分割中的强化学习方法

&#x1f4e2;本篇文章是博主强化学习&#xff08;RL&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…

【C++】优先队列(Priority Queue)全知道

亲爱的读者朋友们&#x1f603;&#xff0c;此文开启知识盛宴与思想碰撞&#x1f389;。 快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 目录 一、前言 二、优先队列&#xff08;Priority Queue&#xff09…

【SQL】实战--组合两个表

题目描述 表: Person ---------------------- | 列名 | 类型 | ---------------------- | PersonId | int | | FirstName | varchar | | LastName | varchar | ---------------------- personId 是该表的主键&#xff08;具有唯一值的列&#xff09;…

STL:相同Size大小的vector和list哪个占用空间多?

在C中&#xff0c;vector和list是两种不同的序列容器。vector底层是连续的内存&#xff0c;而list是非连续的&#xff0c;分散存储的。因此&#xff0c;vector占用的空间更多&#xff0c;因为它需要为存储的元素分配连续的内存空间。 具体占用多少空间&#xff0c;取决于它们分…

《Serverless 架构:引领未来软件开发的新趋势》

一、引言 随着云计算技术的不断发展&#xff0c;软件开发模式也在不断演进。Serverless 架构作为一种新兴的云计算架构模式&#xff0c;正在逐渐改变着软件开发的方式和流程。本文将深入探讨 Serverless 架构的概念、特点、应用场景以及未来发展趋势。 二、Serverless 架构概述…

Java的关键字和保留字

理解什么是关键字&#xff1f; Java赋予了某些单词特殊意义&#xff0c;就不能自己在代码中起同名一样的&#xff0c;否则提示错误 【在Java中关键字都是小写的&#xff0c;并不是所有的小写字母都是关键字&#xff0c;一般在IDEA中显示高亮橘黄色】 理解什么是保留字&#xf…

三十二:HTTP 协议的基本认证

在 Web 开发中&#xff0c;HTTP 协议提供了一种简单的方式来进行身份验证&#xff0c;即 基本认证&#xff08;Basic Authentication&#xff09;。这种认证方式广泛应用于需要保护的资源或 API 接口&#xff0c;它通过在 HTTP 请求头中传递用户名和密码来验证用户身份。虽然基…

GPT vs Claude到底如何选?

美国当地时间6月20日&#xff0c;OpenAI的“劲敌”Anthropic公司发布了最新模型Claude 3.5 Sonnet。据Anthropic介绍&#xff0c;该模型是Claude 3.5系列模型中的首个版本&#xff0c;也是Anthropic迄今为止发布的“最强大、最智能”的模型。它不仅在性能上超越了竞争对手和自家…

Ubuntu 22.04 LTS vs Ubuntu 24.04 LTS:深度剖析,哪个版本更胜一筹?

在开源操作系统领域&#xff0c;Ubuntu一直以其稳定、易用和丰富的功能而受到广泛好评。随着Ubuntu 24的发布&#xff0c;许多用户开始关注这两个版本之间的差异&#xff0c;并考虑是否应该升级到最新版本。鼎峰新匯Benson将对比Ubuntu 22和Ubuntu 24&#xff0c;以帮助用户做出…

Ubuntu 22.04 离线安装软件包

在使用最小化安装时&#xff0c;默认是不带有vim 或者nano编辑器的&#xff0c;如果你的环境不能上外网就需要离线安装。 首先你需要先找一台可以上网的ubuntu系统&#xff08;虚拟机搭建也行&#xff09;&#xff0c;下载所有的依赖包&#xff0c;然后上传到需要安装的服务器…

k8s 1.28 二进制安装与部署

第一步 &#xff1a;配置Linux服务器 #借助梯子工具 192.168.196.100 1C8G kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubectl、haproxy、keepalived 192.168.196.101 1C8G kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubectl、…

unity中:Unity 中异步与协程结合实现线程阻塞的http数据请求

在 Unity 开发中&#xff0c;将协程与 C# 的 async/await 机制结合&#xff0c;可以显著提高代码的可读性与维护性&#xff0c;并且支持返回值。 异步与协程结合在数据请求中的优势 提高代码可读性&#xff1a; 与传统协程相比&#xff0c; async/await 更接近同步逻辑&#xf…

详解QtPDF之 QPdfLink

文章目录 前言QPdfLink 类介绍QPdfLink 的基本功能 QPdfLink 的成员函数1. QPdfLink()2. boundingRect() const3. target() const4. setTarget(const QUrl &target)5. isValid() const 使用 QPdfLink 的示例示例代码代码说明&#xff1a; 总结 前言 在处理 PDF 文档时&…

OGRE 3D----5. OGRE和QML事件交互

在现代图形应用程序开发中,OGRE(Object-Oriented Graphics Rendering Engine)作为一个高性能的3D渲染引擎,广泛应用于游戏开发、虚拟现实和仿真等领域。而QML(Qt Modeling Language)则是Qt框架中的一种声明式语言,专注于设计用户界面。将OGRE与QML结合,可以充分利用OGR…

mysql系列2—InnoDB数据存储方式

背景 本文将深入探讨InnoDB的底层存储机制&#xff0c;包括行格式、页结构、页目录以及表空间等核心概念。通过全面了解这些基础概念&#xff0c;有助于把握MySQL的存储架构&#xff0c;也为后续深入讨论MySQL的索引原理和查询优化策略奠定了基础。 1.行格式 mysql中数据以行…

matlab2024a安装

1.开始安装 2.点击安装 3.选择安装密钥 4.接受条款 5.安装密钥 21471-07182-41807-00726-32378-34241-61866-60308-44209-03650-51035-48216-24734-36781-57695-35731-64525-44540-57877-31100-06573-50736-60034-42697-39512-63953 6 7.选择许可证文件 8.找许可证文件 9.选…

交换机四大镜像(端口镜像、流镜像、VLAN镜像、MAC镜像)应用场景、配置实例及区别对比

在网络管理中&#xff0c;端口镜像、流镜像、VLAN镜像和MAC镜像都是用于监控和分析网络流量的重要技术。 端口镜像&#xff08;Port Mirroring&#xff09; 定义&#xff1a;端口镜像是将一个或多个源端口的流量复制到一个目标端口&#xff0c;以便于网络管理员能够监控和分析…

JVM知识点学习-1

学习视频&#xff1a;狂神说Java 类加载器和双亲委派机制 类加载器 作用&#xff1a;加载Class文件 流程&#xff1a;这里的名字car1。。在栈里面&#xff0c;但是数据在堆里面 类加载器的几个类型&#xff1a; 虚拟机自带的类加载器&#xff1b;启动类&#xff08;根Boot…

Linux下的三种 IO 复用

目录 一、Select 1、函数 API 2、使用限制 3、使用 Demo 二、Poll 三、epoll 0、 实现原理 1、函数 API 2、简单代码模板 3、LT/ET 使用过程 &#xff08;1&#xff09;LT 水平触发 &#xff08;2&#xff09;ET边沿触发 4、使用 Demo 四、参考链接 一、Select 在…

python学习笔记 - python安装与环境变量配置

目录 前言1. 版本选择1.1 什么版本合适&#xff1f;1.2 版本越新越好吗&#xff1f;1.3 维护中的大版本里&#xff0c;选择最早的好吗&#xff1f;1.4 我的选择1.5 Python 发布周期1.6 Python维护中的版本及截止时间 2. 安装包下载2.1 官网地址2.2 下载安装包3. 环境安装3.1 新…