C++之旅(学习笔记)第8章 概念和泛型编程

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

    C++之旅学习笔记第8章 概念和泛型编程

    8.1 引言

    模板第一个最常用的应用是泛型编程泛型编程主要关注通用算法的设计、实现和使用。

    这里“通用”的含义是该算法能支持多种数据类型只要类型符合算法对参数的要求即可。

    模板提供了以下功能

    • 在不丢失信息的情况下将类型以及值和模板作为参数传递的能力。这意味着表达的内容具有很大的灵活性以及具有内联的绝佳机会。
    • 有机会在实例化时将来自不同上下文的信息捏合在一起这意味着有进行针对性优化的可能。
    • 把值作为模板参数传递的能力也就是在编译时计算的能力。

    8.2 概念

    template<typename Seq, typename Value>
    Value sum(Seq s, Value v) {
        for(const auto& x : s)
            v+=x;
        return v;
    }
    

    这个sum函数需要保证

    • 它的第一个模板参数是某种元素序列Seq它支持begin()和end()从而允许范围for语句正常工作。
    • 第二个参数是某种形式的数字Value支持+=因此元素可以被累加。

    这种需求叫作概念。

    满足作为序列要求的大致有标准库vector、list、map

    满足作为算术类型的大致有int、double、Matrix所有合理定义的矩阵都支持算术运算。

    从以下两个维度看sum属于通用算法

    • 数据结构的类型序列存储方式维度
    • 数据元素的类型维度。

    8.2.1 概念的运用

    大多数模板参数必须符合特定需求才能被正常编译和运行。也就是说绝大多数模板都应当是受限模板。

    类型名称指示符typename是限定程度最低的他仅仅要求该参数是一个类型。

    template<Sequence Seq, Number Num>
    Num sum(Seq s, Num v){
        for(const auto& x : s)
            v+=x;
        return v;
    }
    
    • 这里的sum定义了Sequence和Number这两个概念的实际含义编译器就可从sum的接口中直接识别出无效的实例化调用而无需等到编译或者运行时才能报告错误。

    以下这些巴拉巴拉一大堆也不知道讲的什么做个标记以后再看

    先看看GPT的解释

    强调了使用概念来对模板参数进行约束的重要性以提高代码的清晰度和安全性。通过使用概念可以在编译期间发现一些错误而不是等到运行时才暴露问题。

    在C++中requires 子句是用于指定模板参数的一组要求constraints的关键字。它用于在模板定义中对模板参数进行约束以确保只有符合指定条件的类型或值才能被接受。

    在模板中requires 子句通常用于 requires 关键字之后用于指定一组布尔表达式这些表达式描述了模板参数必须满足的条件。如果这些条件不满足编译器将拒绝对该模板的实例化并在编译时生成错误消息。

    例如在上面提到的代码中requires Arithmetic<range_value_t<Seq>, Num> 表达了对于类型 range_value_t<Seq>Num必须满足 Arithmetic 概念。这样的约束有助于确保在模板函数中对这些类型进行算术运算时是安全和合法的。

    requires 子句的使用使得模板的错误能够更早地在编译期间被发现提高了代码的可读性和安全性。在概念引入之前开发者通常通过模板的SFINAESubstitution Failure Is Not An Error机制来实现类似的效果但概念提供了更为直观和清晰的方式来表达对模板参数的要求。

    但是sum接口的技术规格不太完整应该允许将整个Sequence的元素累加到Number。

    template<Sequence Seq, Number Num>
    	requires Arithmetic<range_value_t<Seq>,Num>
    Num sum(Seq s, Num s);
    
    • 序列的range_value_t是序列中的元素类型它来自标准库中range的类型名称。
    • Arithmetic<X,Y>则是一个概念它表明X与Y可以进行算术运算。这样可避免vector<string>vector<int>的sum()这样的操作。同时也可以正常支持vector<int>vector<complex<double>>这样的参数。
    • 在这个例子中我们只需要+=操作符。但不需要限制的那么死或许某一天我们需要用+和=两个操作符来替代+=操作符就会庆幸使用了更通用的概念Arithmetic而不是单纯限制为“有用+=操作符”。

    requires Arithmetic<range_value_t<Seq>,Num>被称作requirements子句。其中记法template<Sequence Seq>就是比requires Sequence<Seq>更简单的写法。

    复杂点则等价于

    template<Sequence Seq, Number Num>
    	requires Arithmetic<Seq> && Number<Num> && Arithmetic<range_value_t<Seq>,Num>
    Num sum(Seq s, Num s);
    

    同时写成如下简写形式也具有等价的效果

    template<<Sequence Seq, Arithmetic<range_value_t<Seq>> Num>
    Num sum(Seq s, Num n);
    

    8.2.2 基于概念的重载

    一旦我们正确地指定了模板地接口就可以根据它们地属性进行重载如同函数一样。

    例如标准库advance()函数向前移动迭代器简化版本如下

    template<forward_iterator Iter>
    void advance(Iter p, int n) {	// 将p向前移动n个元素
        while(n--)
            ++p;					// 前向迭代器拥有 ++ 操作符但没有+或者+=操作符
    }
    
    template<random_access_iterator Iter>
    void advance(Iter p, int n) {	// 将p向前移动n个元素
        p += n;						// 随机访问迭代器拥有 += 操作符
    }
    

    编译器会选择满足最严格参数需求的版本。list只提供了向前迭代器vector提供了随机访问迭代器。

    因此

    void user(vector<int>::iterator vip, list<string>::iterator lsp)
    {
        advance(vip,10);	// 使用快速版本的advance()
        advance(lsp,10);	// 使用慢速版本的advance()
    }
    

    如同其他的重载这是编译时机制没有任何开销

    如果编译器无法找到最佳选择会报二义性错误。

    考虑具有一个参数并且提供多个版本的模板函数

    • 如果参数不能匹配特定概念那么那个版本不会被选择。
    • 如果参数可以匹配概念并且存在唯一匹配那么选择那个版本。
    • 如果参数可以同时匹配两个版本的概念但其中一个概念比另外一个更严格其中一个概念时另外一个概念的完整子集那么选择更严格的那一个。
    • 如果参数匹配两个概念并且无法判断两个概念谁更严格那么会报二义性错误。

    选择某个特定版本的模板必须满足这些条件

    • 匹配所有参数并且
    • 至少有一个参数与其他版本的匹配度均等并且
    • 至少有一个参数是最佳匹配。

    8.3 泛型编程

    C++直接支持的泛型编程形式围绕着这样的思想从具体、高效的算法中抽象出来从而获得可以与不同数据表示相结合的泛型算法以生成各种有用的软件。

    表示基本操作和数据结构的抽象被称为概念。

    …较为复杂先不管以后有能力再看

    8.4 可变参数模板

    定义模板时可以令其接受任意数量、任意类型的实参这样的模板被称为可变参数模板。

    假设我们需要实现一个简单的函数输出任意可以被 << 操作符输出的数据

    void user() {
        print("first: ", 1, 2.2, "hello\n"s);						// 输出first: 1 2.2 hello
        printf("\nsecond: ", 0.2, 'c', "yuck!"s, 0, 1, 2, '\n');	// 输出second: 0.2 c yuck! 0 1 2
    }
    

    传统方法是实现一个可变参数模板将第一个参数剥离出来然后用递归调用的办法处理所有剩下的参数

    template<typename T>
    concept Printable = requires(T t) { std::cout << t; }	// 只有一个操作
    void print()
    {
        // 处理无参数的情况什么都不做
    }
    template<Printable T, Printable... Tail>
    void print()
    {
        cout << head << ' ';		// 首先对head进行操作
        print(tail...);				// 然后操作tail
    }
    
    • 这里加了省略号的Printable…表示Tail包含多个类型的序列。
    • 而Tail…则表示tail本身是这个序列的值。参数声明后面加了省略号…这叫作参数包。
    • 这里的tail是由函数参数组成的参数包其元素类型对应的是Tail模板参数包中指定的类型。
    • 使用这样的机制print()可以接受任意数量、任意类型的参数。

    每次调用print()都把参数分成头元素以及其他尾元素。对头元素调用了打印命令然后对其他元素调用print()。最终tail变为空所以我们一定需要一个无参数的版本来处理空参数的情况。如果不需要处理无参数的情况可以通过编译时if来消除这种情况。

    template<Printable T, Printable... Tail>
    void print(T head, Tail... tail)
    {
        cout << head << ' ';
        if constexpr(sizeof...(tail) > 0)
            print(tail...);
    }
    

    这里使用编译时if而不是运行时if可以避免生成对空参数print()函数的调用。这也就无须定义空参数版本的print()。

    可变参数模板的强大之处在于它们可以接受任意参数。缺点包括

    • 递归实现需要一些技巧容易出错。
    • 很可能需要一个精心设计的模板程序才能方便地对接口地类型进行有效检查。
    • 类型检查代码是临时地而不是被标准定义地。
    • 递归实现在编译时地开销可能非常昂贵也会占用大量地编译器内存。

    8.4.1 折叠表达式

    template<Number... T>
    int sum(T... v)
    {
        return (v + ... + 0);	// 将v中所有元素与0累和
    }
    

    这里(v + … + 0)表示把v中的所有元素加起来从0开始。首先做加法的元素是最右边的那个也就是索引最大的那个v[0]+(v[1]+(v[2]+(v[3]+(v[4]+0))))。从右边开始的叫作右折叠。

    这个sum()函数可以接受任意数量、任意类型的参数

    int x = sum(1,2,3,4,5);		// x变成15
    int y = sum('a', 2.4, x);	// y变成1142.4被取整‘a'的值是97
    

    反之左折叠

    template<Number... T>
    int sum(T... v)
    {
        return (0 + ... + v);	// 将v中所有元素与0累和
    }
    

    (((((0+v[0])+v[1])+v[2])+v[3])+v[4])

    除此之外折叠表达式不仅限于算术操作。

    template<Printable ...T>
    void print(T&&... args)
    {
        (std::cout << ... << args) << '\n';		// 打印输出所有参数
    }
    // (((((std::cout << "Hello!"s) << ' ') << "World ") << 2017) << '\n');
    print("Hello!"s,' ',"World ",2017);
    

    出现2017是因为fold()的特性实在C++2017标准中被添加的。

    8.4.2 完美转发参数

    使用可变参数模板时保证参数在通过接口传递的过程中完全不变有时非常有用。

    8.5 建议

    。。。

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

    “C++之旅(学习笔记)第8章 概念和泛型编程” 的相关文章

    消息队列

    msgget函数用于创建一个新的消息队列或访问一个已存在的消息队列 IPC_NOWAIT标志使得msgsend调用非阻塞:如果没有存放新消息的可用空间,该函数就马上返回.这个条件可能发生的情况包括: 1.在指定的队列中已有太多的字节 2.在系统范围存在太多的...

    浅谈Dubbo的异步调用

    目录 Dubbo消费端实现 Dubbo调用的实现——DubboInvoker  Dubbo异步调用实现——AsyncToSyncInvoker 如何使用异步调用...

    ubuntu 18.04 搜狗输入法 安装步骤【已成功】

    前言搜狗输入法在Ubuntu使用的是真心香Ubuntu输入法真的不是很好用。但是经历过好几次始终不好安装。最终安装成功可以继续搬砖了 第一步安装sogoupinyin_4.0.1.2800_x86_64.deb包 通过命令行重新安装搜狗输入法安装包官网下载链接 sudo dpkg –i sog...

    增删改查sql语法

    sql中增删改查语句 1、“INSERT INTO”语句用于向表格中增加新的行 2、“DELETE”语句用于删除表中的行 3、“Update”语句用于修改表中的数据 4、“SELECT”语句用于从表中选取数据 一、增加语法 INSERT INTO 表名 VALUES (值1,....) 例如...

    怎么用Python获取和存储时间序列数据 - 编程语言

    今天小编给大家分享一下怎么用Python获取和存储时间序列数据的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。 要求本教程在通...

    PHP怎么实现异步定时多任务消息推送 - 开发技术

    这篇“PHP怎么实现异步定时多任务消息推送”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“PHP怎么实现异步定时多任务消息推送”文章吧。在 PHP 中实现...