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约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。
  • 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI开发:逻辑回归 - 实战演练- 垃圾邮件的识别(二)

接上一篇AI开发&#xff1a;逻辑回归 - 实战演练- 垃圾邮件的识别&#xff08;一&#xff09; new_email 无论为什么文本&#xff0c;识别结果几乎都是垃圾邮件,因此我们需要对源码的逻辑进行梳理一下&#xff1a; 在代码中&#xff0c;new_email 无论赋值为何内容都被识别为…

字符串处理(二)

第1题 篮球比赛 查看测评数据信息 学校举行篮球比赛&#xff0c;请设计一个计分系统统计KIN、WIN两队分数&#xff0c;并输出分数和结果&#xff01; 如果平分就输出‘GOOD’&#xff0c;否则输出获胜队名&#xff01; 输入格式 输入数据共n1行&#xff0c; 第1行n&#xf…

【数据库系列】Liquibase 与 Flyway 的详细对比

在现代软件开发中&#xff0c;数据库版本控制是一个至关重要的环节。为了解决数据库迁移和变更管理的问题&#xff0c;开发者们通常会使用工具&#xff0c;如 Liquibase 和 Flyway。本文将对这两个流行的数据库迁移工具进行详细比较&#xff0c;从基础概念、原理、优缺点到使用…

企业品牌曝光的新策略:短视频矩阵系统

企业品牌曝光的新策略&#xff1a;短视频矩阵系统 在当今数字化时代&#xff0c;短视频已经渗透到我们的日常生活之中&#xff0c;成为连接品牌与消费者的关键渠道。然而&#xff0c;随着平台于7月20日全面下线了短视频矩阵的官方接口&#xff0c;许多依赖于此接口的小公司和内…

PostgreSQL最常用数据类型-重点说明自增主键处理

简介 PostgreSQL提供了非常丰富的数据类型&#xff0c;我们平常使用最多的基本就3类&#xff1a; 数字类型字符类型时间类型 这篇文章重点介绍这3中类型&#xff0c;因为对于高并发项目还是推荐&#xff1a;尽量使用简单类型&#xff0c;把运算和逻辑放在应用中&#xff0c;…

做异端中的异端 -- Emacs裸奔之路4: 你不需要IDE

确切地说&#xff0c;你不需要在IDE里面编写或者阅读代码。 IDE用于Render资源文件比较合适&#xff0c;但处理文本&#xff0c;并不划算。 这的文本文件&#xff0c;包括源代码&#xff0c;配置文件&#xff0c;文档等非二进制文件。 先说说IDE带的便利: 函数或者变量的自动…

ospf协议(动态路由协议)

ospf基本概念 定义 OSPF 是典型的链路状态路由协议&#xff0c;是目前业内使用非常广泛的 IGP 协议之一。 目前针对 IPv4 协议使用的是 OSPF Version 2 &#xff08; RFC2328 &#xff09;&#xff1b;针对 IPv6 协议使用 OSPF Version 3 &#xff08; RFC2740 &#xff09;。…

【热门主题】000072 分布式数据库:开启数据管理新纪元

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【热…

Python 3 教程第33篇(MySQL - mysql-connector 驱动)

Python MySQL - mysql-connector 驱动 MySQL 是最流行的关系型数据库管理系统&#xff0c;如果你不熟悉 MySQL&#xff0c;可以阅读我们的 MySQL 教程。 本章节我们为大家介绍使用 mysql-connector 来连接使用 MySQL&#xff0c; mysql-connector 是 MySQL 官方提供的驱动器。…

ENSP IPV6-over-IPV4

IPv6是网络层协议的第二代标准协议&#xff0c;一个IPv6地址同样可以分为网络前缀和主机ID两个部分。 可以将IPV4的网络看成IPV6的承载网&#xff0c;只有IPv4网络是连通的&#xff0c;则IPv6网络才有可能连通。所以配置的时候需要先配置IPv4网络的路由功能&#xff0c;再配IP…

《数据挖掘:概念、模型、方法与算法(第三版)》

嘿&#xff0c;数据挖掘的小伙伴们&#xff01;今天我要给你们介绍一本超级实用的书——《数据挖掘&#xff1a;概念、模型、方法与算法》第三版。这本书是数据挖掘领域的经典之作&#xff0c;由该领域的知名专家编写&#xff0c;系统性地介绍了在高维数据空间中分析和提取大量…

53 基于单片机的8路抢答器加记分

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 首先有三个按键 分别为开始 暂停 复位&#xff0c;然后八个选手按键&#xff0c;开机显示四条杠&#xff0c;然后按一号选手按键&#xff0c;数码管显示&#xff13;&#xff10;&#xff0c;这…

从零开始写游戏之斗地主-网络通信

在确定了数据结构后&#xff0c;原本是打算直接开始写斗地主的游戏运行逻辑的。但是突然想到我本地写出来之后&#xff0c;也测试不了啊&#xff0c;所以还是先写通信模块了。 基本框架 在Java语言中搞网络通信&#xff0c;那么就得请出Netty这个老演员了。 主要分为两个端&…

Logistic Regression(逻辑回归)、Maximum Likelihood Estimatio(最大似然估计)

Logistic Regression&#xff08;逻辑回归&#xff09;、Maximum Likelihood Estimatio&#xff08;最大似然估计&#xff09; 逻辑回归&#xff08;Logistic Regression&#xff0c;LR&#xff09;逻辑回归的基本思想逻辑回归模型逻辑回归的目标最大似然估计优化方法 逻辑回归…

数据类型.

数据类型分类 数值类型 tinyint类型 以tinyint为例所有数值类型默认都是有符号的&#xff0c;无符号的需要在后面加unsignedtinyint的范围在-128~127之间无符号的范围在0~255之间(类比char) create database test_db; use test_db;建表时一定要跟着写上属性 mysql> creat…

IDEA使用HotSwapHelper进行热部署

目录 前言JDK1.8特殊准备DECVM安装插件安装与配置参考文档相关下载 前言 碰到了一个项目&#xff0c;用jrebel启动项目时一直报错&#xff0c;不用jrebel时又没问题&#xff0c;找不到原因&#xff0c;又不想放弃热部署功能 因此思考能否通过其他方式进行热部署&#xff0c;找…

机器学习算法(六)---逻辑回归

常见的十大机器学习算法&#xff1a; 机器学习算法&#xff08;一&#xff09;—决策树 机器学习算法&#xff08;二&#xff09;—支持向量机SVM 机器学习算法&#xff08;三&#xff09;—K近邻 机器学习算法&#xff08;四&#xff09;—集成算法 机器学习算法&#xff08;五…

【Electron学习笔记(四)】进程通信(IPC)

进程通信&#xff08;IPC&#xff09; 进程通信&#xff08;IPC&#xff09;前言正文1、渲染进程→主进程&#xff08;单向&#xff09;2、渲染进程⇌主进程&#xff08;双向&#xff09;3、主进程→渲染进程 进程通信&#xff08;IPC&#xff09; 前言 在Electron框架中&…

GateWay使用手册

好的&#xff0c;下面是优化后的版本。为了提高可读性和规范性&#xff0c;我对内容进行了结构化、简化了部分代码&#xff0c;同时增加了注释说明&#xff0c;便于理解。 1. 引入依赖 在 pom.xml 中添加以下依赖&#xff1a; <dependencies><!-- Spring Cloud Gate…

【Go 基础】channel

Go 基础 channel 什么是channel&#xff0c;为什么它可以做到线程安全 Go 的设计思想就是&#xff1a;不要通过共享内存来通信&#xff0c;而是通过通信来共享内存。 前者就是传统的加锁&#xff0c;后者就是 channel。也即&#xff0c;channel 的主要目的就是在多任务间传递…