现代C++新特性 扩展的聚合类型(C++17 C++20)(PC浏览效果更佳)

    文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载 

1.聚合类型的新定义

C++17标准对聚合类型的定义做出了大幅修改,即从基类公开且非虚继承的类也可能是一个聚合。同时聚合类型还需要满足常规条件。

1.没有用户提供的构造函数。

2.没有私有和受保护的非静态数据成员。

3.没有虚函数。

在新的扩展中,如果类存在继承关系,则额外满足以下条件。

4.必须是公开的基类,不能是私有或者受保护的基类。

5.必须是非虚继承。

请注意,这里并没有讨论基类是否需要是聚合类型,也就是说基类是否是聚合类型与派生类是否为聚合类型没有关系,只要满足上述5 个条件,派生类就是聚合类型。在标准库<type_traits>中提供了一个聚合类型的甄别办法is_aggregate,它可以帮助我们判断目标类型是否为聚合类型:

#include <iostream>
#include <string>class MyString : public string {};int main(int argc, char** argv)
{cout << "is_aggregate_v<string> = " << is_aggregate_v<string> << endl;cout << "is_aggregate_v<MyString> = " << is_aggregate_v<MyString> << endl;}

在上面的代码中,先通过is_aggregate_v判断string是否为聚合类型,根据我们对string的了解,它存在用户提供的构造函数,所以一定是非聚合类型。然后判断类

MyString是否为聚合类型,虽然该类继承了string,但因为它是公开继承且是非虚继承,另外,在类中不存在用户提供的构造函数、虚函数以及私有或者受保护的数据成员,所以MyString应该是聚合类型。编译运行以上代码,输出的结果也和我们判断的一致:

is_aggregate_v<string> = 0
is_aggregate_v<MyString> = 1

2.聚合类型的初始化

由于聚合类型定义的扩展,聚合对象的初始化方法也发生了变化。过去要想初始化派生类的基类,需要在派生类中提供构造函数,例如:

#include <iostream>
#include <string>class MyStringWithIndex : public string {
public:MyStringWithIndex(const string& str, int idx) : string(str), index_(idx) {}int index_ = 0;
};ostream& operator << (ostream& o, const MyStringWithIndex& s)
{o << s.index_ << ":" << s.c_str();return o;
}int main(int argc, char** argv)
{MyStringWithIndex s("hello world", 11);cout << s << endl;
}

在上面的代码中,为了初始化基类我们不得不为MyStringWithIndex提供一个构造函数,用构造函数的初始化列表来初始化string。现在,由于聚合类型的扩展,这个过程得到了简化。需要做的修改只有两点,第一是删除派生类中用户提供的构造函数,第二是直接初始化:

#include <iostream>
#include <string>class MyStringWithIndex : public string {
public:int index_ = 0;
};ostream& operator << (ostream& o, const MyStringWithIndex& s)
{o << s.index_ << ":" << s.c_str();return o;
}int main(int argc, char** argv)
{MyStringWithIndex s{ {"hello world"}, 11 };cout << s << endl;
}

删除派生类中用户提供的构造函数是为了让MyStringWithIndex成为一个C++17标准的聚合类型,而作为聚合类型直接使用大括号初始化即可。MyStringWithIndex s{{"hello world"}, 11}是典型的初始化基类聚合类型的方法。其中{"hello world"}用于基类的初始化,11用于index_的初始化。这里的规则总是假设基类是一种在所有数据成员之前声明的特殊成员。所以实际上,{"hello world"}的大括号也可以省略,直接使用MyStringWithIndex s{ "hello world", 11}也是可行的。另外,如果派生类存在多个基类,那么其初始化的顺序与继承的顺序相同:

#include <iostream>
#include <string>class Count {
public:int Get(){return count_++;}int count_ = 0;
};class MyStringWithIndex : public string, public Count {
public:int index_ = 0;
};ostream& operator << (ostream& o, MyStringWithIndex& s)
{o << s.index_ << ":" << s.Get() << ":" << s.c_str();return o;
}int main(int argc, char** argv)
{MyStringWithIndex s{ "hello world", 7, 11 };cout << s << endl;cout << s << endl;
}

在上面的代码中,类MyStringWithIndex先后继承了string和Count,所以在初始化时需要按照这个顺序初始化对象。{ "hello world", 7, 11}中字符串"hello world"对应基类string,7对应基类Count,11对应数据成员 index_。

​​​​​​​3.扩展聚合类型的兼容问题

虽然扩展的聚合类型给我们提供了一些方便,但同时也带来了一个兼容老代码的问题,请考虑以下代码:

#include <iostream>
#include <string>class BaseData {int data_;
public:int Get(){return data_;}
protected:BaseData() : data_(11) {}
};class DerivedData : public BaseData {
public:
};int main(int argc, char** argv)
{DerivedData d{};cout << d.Get() << endl;
}

以上代码使用C++11或者C++14标准可以编译成功,而使用C++17标准编译则会出现错误,主要原因就是聚合类型的定义发生了变化。在C++17之前,类DerivedData不是一个聚合类型,所以DerivedData d{}会调用编译器提供的默认构造函数。调用DerivedData默认构造函数的同时还会调用BaseData的构造函数。

虽然这里BaseData声明的是受保护的构造函数,但是这并不妨碍派生类调用它。从C++17开始情况发生了变化,类DerivedData变成了一个聚合类型,以至于DerivedData d{}也跟着变成聚合类型的初始化,因为基类BaseData中的构造函数是受保护的关系,它不允许在聚合类型初始化中被调用,所以编译器无奈之下给出了一个编译错误。如果读者在更新开发环境到C++17标准的时候遇到了这样的问题,只需要为派生类提供一个默认构造函数即可。

​​​​​​​4.禁止聚合类型使用用户声明的构造函数

在前面我们提到没有用户提供的构造函数是聚合类型的条件之一,但是请注意,用户提供的构造函数和用户声明的构造函数是有区别的,比如:

#include <iostream>struct X {X() = default;
};struct Y {Y() = delete;
};int main(int argc, char** argv)
{cout << boolalpha << "is_aggregate_v<X> : " << is_aggregate_v<X> << endl;cout << "is_aggregate_v<Y> : " << is_aggregate_v<Y> << endl;
}

用C++17标准编译运行以上代码会输出:

is_aggregate_v<X> : true
is_aggregate_v<Y> : true

由此可见,虽然类X和Y都有用户声明的构造函数,但是它们依旧是聚合类型。不过这就引出了一个问题,让我们将目光放在结构体Y 上,因为它的默认构造函数被显式地删除了,所以该类型应该无法实例化对象,例如:

Y y1; // 编译失败,使用了删除函数

但是作为聚合类型,我们却可以通过聚合初始化的方式将其实例化:

Y y2{}; // 编译成功

编译成功的这个结果显然不是类型Y的设计者想看到的,而且这个问题很容易在真实的开发过程中被忽略,从而导致意想不到的结果。除了删除默认构造函数,将其列入私有访问中也会有同样的问题,比如:

struct Y {
private:Y() = default;
};
Y y1;   // 编译失败,构造函数为私有访问
y y2{}; // 编译成功

请注意,这里Y() = default;中的= default不能省略,否则Y会被识别为一个非聚合类型。

为了避免以上问题的出现,在C++17标准中可以使用explicit说明符或者将= default声明到结构体外,例如

struct X {explicit X() = default;
};struct Y {Y();
};Y::Y() = default;

这样一来,结构体X和Y被转变为非聚合类型,也就无法使用聚合初始化了。不过即使这样,还是没有解决相同类型不同实例化方式表现不一致的尴尬问题,所以在C++20标准中禁止聚合类型使用用户声明的构造函数,这种处理方式让所有的情况保持一致,是最为简单明确的方法。同样是本节中的第一段代码示例,用C++20环境编译的输出结果如下

is_aggregate_v<X> : false
is_aggregate_v<Y> : false

值得注意的是,这个规则的修改会改变一些旧代码的意义,比如我们经常用到的禁止复制构造的方法:

struct X {string s;vector<int> v;X() = default;X(const X&) = delete;X(X&&) = default;
};

上面这段代码中结构体X在C++17标准中是聚合类型,所以可以使用聚合类型初始化对象。但是升级编译环境到C++20标准会使X转变为非聚合对象,从而造成无法通过编译的问题。一个可行的解决方案是,不要直接使用= delete;来删除复制构造函数,而是通过加入或者继承一个不可复制构造的类型来实现类型的不可复制,例如

struct X {string s;vector<int> v;[[no_unique_address]] NonCopyable nc;
};// 或者
struct X : NonCopyable {string s;vector<int> v;
};

这种做法能让代码看起来更加简洁,所以我们往往会被推荐这样做。

​​​​​​​5.使用带小括号的列表初始化聚合类型对象

通过2中,我们知道对于一个聚合类型可以使用带大括号的列表对其进行初始化,例如

struct X {int i;float f;
};X x{ 11, 7.0f };

如果将上面初始化代码中的大括号修改为小括号,C++17标准的编译器会给出无法匹配到对应构造函数X::X(int, float)的错误,这说明小括号会尝试调用其构造函数。这一点在C++20标准中做出了修改,它规定对于聚合类型对象的初始化可以用小括号列表来完成,其最终结果与大括号列表相同。所以以上代码可以修改为

X x(11, 7.0f);

另外,前面的章节曾提到过带大括号的列表初始化是不支持缩窄转换的,但是带小括号的列表初始化却是支持缩窄转换的,比如

struct X {int i;short f;
};X x1{ 11, 7.0 }; // 编译失败,7.0从double转换到short是缩窄转换
X x2(11, 7.0);   // 编译成功

需要注意的是,到目前为止该特性只在GCC中得到支持,而CLang 和MSVC都还没有支持该特性。

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

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

相关文章

Django_模型类详解(七)

目录 一、定义属性 字段类型 选项 二、查询集 两大特性 查询集的缓存 限制查询集 三、条件查询 条件运算符 1) 查询相等 2) 模糊查询 3) 空查询 4) 范围查询 5) 比较查询 6) 日期查询 F对象 Q对象 聚合函数 四、关联查询 通过对象执行关联查询 通过模型类执…

Squid 缓存代理(一)---原理及搭建(传统代理、透明代理)

前言 Squid 是 Linux 系 统 中 最 常 用 的 一 款 开 源 代 理 服 务 软 件 &#xff08; 官 方 网 站 为 http://www.squid-cache.org&#xff09;&#xff0c;可以很好地实现 HTTP 和 FTP&#xff0c;以及 DNS 查询、SSL 等应用的缓存代理。缓存代理作为应用层的代理服务软件…

Python学习笔记-基于socket基础的http服务端程序

通过HTTP协议可以进行通信可以规范化的进行网络间通信。下面技术第一个http服务器小程序。简单的记录第一个试手程序。 1.http通信的基本流程 整个流程对应四层网络架构&#xff1a;应用层、传输层、网络层、链路层。有的部分已经封装&#xff0c;不需要我们再行处理。 2.服务…

SAR ADC version2 ——ADC背景介绍

目录&#xff1a; ADC常用指标分类 静态性能&#xff1a;微分非线性&#xff1a;DNL 积分非线性&#xff1a;INL 仿真测试DNL&#xff1a;&#xff08;码密度法&#xff09;&#xff08;code density&…

1.Git使用技巧-常用命令1

Git使用技巧-常用命令 文章目录 Git使用技巧-常用命令一、git 创建仓库demo 二、本地仓库常用命令提交详解git commitgit commit --amend 三、 推送到远程分支git push 总结参考 一、git 创建仓库 创建远端仓库&#xff1a; git init – bare // 创建远端裸仓库&#xff1b; 远…

哈工大计算网络课程数据链路层详解之:数据链路层服务

哈工大计算网络课程数据链路层详解之&#xff1a;数据链路层服务 在介绍完网络层的实现功能和协议之后&#xff0c;接下来我们继续介绍网络层的下一层&#xff1a;数据链路层。 本节首先对数据链路层的功能和所提供的服务进行概述。 如下图示例网络所示&#xff0c;标红色的部…

18.Lucas-Kanade光流及OpenCV中的calcOpticalFlowPyrLK

文章目录 光流法介绍OpenCV中calcOpticalFlowPyrLK函数补充reference 欢迎访问个人网络日志&#x1f339;&#x1f339;知行空间&#x1f339;&#x1f339; 光流法介绍 光流描述了像素在图像中的运动&#xff0c;就像彗星☄划过天空中流动图像。同一个像素&#xff0c;随着时…

【三】部署zabbix-proxy代理服务器和高可用,以及监控windows系统和java应用

zabbix代理服务器和高可用 1.部署zabbix代理服务器1.1 代理端zabbix-proxy配置1.2 客户端zabbix-agent配置1.3 zabbix-proxy总结 2. 部署Zabbix高可用集群2.1 主节点zabbix-server配置2.2 备节点zabbix-server配置2.3 客户端zabbix_agent配置2.4 Zabbix高可用集群总结 3.Zabbix…

【uniapp开发小程序】实现读取手机剪切板第一条,识别并以姓名/手机号/收货地址格式输出

效果图&#xff1a; 完整代码&#xff1a; <template><view class""><text>测试</text><view click"pasteContent()" class"content">点击此处可快速识别 您的收货信息</view></view> </templat…

WAIC2023记录

汤晓鸥 学生与工作&#xff1a; 微软布局大模型方向

Linux高频常用指令汇总

目录 认识 Linux 目录结构 绝对路径&#xff1a;以根目录开头的, 称为绝对路径 相对路径&#xff1a;不是根目录开头的,称为相对路径 ls pwd cd mkdir touch cat echo rm cp mv vim编辑器 1、进入文件 2、进行编辑模式 3、保存退出 重要的几个热键[Tab],[ct…

如何利用Spine制作简单的2D骨骼动画

在2D游戏中&#xff0c;我们经常看到各种各样的角色动画。动画能给游戏带来生机和灵气。创作一段美妙的动画&#xff0c;不仅需要强大的软件工具&#xff0c;更需要一套完善的工作流程。 Spine就是一款针对游戏开发的2D骨骼动画编辑工具。Spine 可以提供更高效和简洁 的工作流…