网易首页 > 网易号 > 正文 申请入驻

C++17 在业务代码中最好用的十个特性

0
分享至

作者:jinshang,腾讯 WXG 后台开发工程师

自从步入现代 C++时代开始,C++语言标准形成了三年一个版本的惯例:C++11 标志着现代 C++的开端,C++14 在 11 的基础上查缺补漏,并未加入许多新特性,而 C++17 作为 C++11 后的第一个大版本,标志着现代 C++逐渐走向成熟。WXG 编译器升级到 gcc7.5 已有一段时间,笔者所在项目组也已经将全部代码升级到 C++17。在使用了 C++17 一年多之后,笔者总结了 C++17 在业务代码中最好用的十个特性。

注 1:本文只包含 wxg 的 gcc7.5 支持的特性,, 等暂不支持的特性不包含在内。

注 2:本文只包含应用于业务逻辑的特性,, 等适用于元编程和科学计算的特性并不包含。

笔者将这些特性大体上分为三类:语法糖、性能提升和类型系统。

语法糖

这里所说的语法糖,并不是严格意义上编程语言级别的语法糖,还包括一些能让代码更简洁更具有可读性的函数和库:

c++17 最便利的语法糖当属结构化绑定。结构化绑定是指将 array、tuple 或 struct 的成员绑定到一组变量*上的语法,最常用的场景是在遍历 map/unordered_map 时不用再声明一个中间变量了:

// pre c++17
for(const auto& kv: map){
const auto& key = kv.first;
const auto& value = kv.second;

// c++17
for(const auto& [key, value]: map){
// ...
}

*: 严格来说,结构化绑定的结果并不是变量,c++标准称之为名字/别名,这也导致它们不允许被 lambda 捕获,但是 gcc 并没有遵循 c++标准,所以以下代码在 gcc 可以编译,clang 则编译不过

for(const auto& [key, value]: map){
[&key, &value]{
std::cout << key << ": " << value << std::endl;

在 clang 环境下,可以在 lambda 表达式捕获时显式引入一个引用变量通过编译

for(const auto& [key, value]: map){
[&key = key, &value = value]{
std::cout << key << ": " << value << std::endl;

另外这条限制在 c++20 中已经被删除,所以在 c++20 标准中 gcc 和 clang 都可以捕获结构化绑定的对象了。

在 c++17 以前,构造std::pair/std::tuple时必须指定数据类型或使用std::make_pair/std::make_tuple函数,c++17 为std::pair/std::tuple新增了推导规则,可以不再显示指定类型。

// pre c++17
std::pair p1{3.14, "pi"s};
auto p1 = std::make_pair(3.14, "pi"s);

// c++17
std::pair p3{3.14, "pi"s};

if constexpr 语句是编译期的 if 判断语句,在 C++17 以前做编译期的条件判断往往通过复杂机制或模版重载实现,甚至嫌麻烦的时候直接放到运行时用 if 判断,造成性能损耗,if constexpr 大大缓解了这个问题。比如我想实现一个函数将不同类型的输入转化为字符串,在 c++17 之前需要写三个函数去实现,而 c++17 只需要一个函数。

// pre c++17
template
std::string convert(T input){
return std::to_string(input);

// const char*和string进行特殊处理
std::string convert(const char* input){
return input;
}
std::string convert(std::string input){
return input;
}
// c++17
template
std::string convert(T input) {
if constexpr (std::is_same_vconst char*> ||
std::is_same_vstd::string>) {
return input;
} else {
return std::to_string(input);
}
}

c++17 支持在 if 的判断语句之前增加一个初始化语句,将仅用于 if 语句内部的变量声明在 if 内,有助于提升代码的可读性。且对于 lock/iterator 等涉及并发/RAII 的类型更容易保证程序的正确性。

// c++ 17
std::map m;
std::mutex mx;
extern bool shared_flag; // guarded by mx

int demo()
{
if (auto it = m.find(10); it != m.end()) { return it->second.size(); }
if (char buf[10]; std::fgets(buf, 10, stdin)) { m[0] += buf; }
if (std::lock_guard lock(mx); shared_flag) { unsafe_ping(); shared_flag = false; }
if (int s; int count = ReadBytesWithSignal(&s)) { publish(count); raise(s); }
if (const auto keywords = {"if", "for", "while"};
std::ranges::any_of(keywords, [&tok](const char* kw) { return tok == kw; }))
{
std::cerr << "Token must not be a keyword\n";
}
}
性能提升

shared_mutex是 c++的原生读写锁实现,有共享和独占两种锁模式,适用于并发高的读场景下,通过 reader 之前共享锁来提升性能。在 c++17 之前,只能自己通过独占锁和条件变量自己实现读写锁或使用 c++14 加入的性能较差的std::shared_timed_mutex。以下是通过shared_mutex实现的线程安全计数器:

// c++17
class ThreadSafeCounter {
public:
ThreadSafeCounter() = default;

// Multiple threads/readers can read the counter's value at the same time.
unsigned int get() const {
std::shared_lock lock(mutex_);
return value_;
}

// Only one thread/writer can increment/write the counter's value.
unsigned int increment() {
std::unique_lock lock(mutex_);
return ++value_;
}

// Only one thread/writer can reset/write the counter's value.
void reset() {
std::unique_lock lock(mutex_);
value_ = 0;
}

private:
mutable std::shared_mutex mutex_;
unsigned int value_ = 0;
};

std::string_view顾名思义是字符串的“视图”,类成员变量包含两个部分:字符串指针和字符串长度,std::string_view 涵盖了 std::string 的所有只读接口。std::string_view 对字符串不具有所有权,且兼容 std::string 和 const char*两种类型。

c++17 之前,我们处理只读字符串往往使用const std::string&std::string有两点性能优势:

  1. 兼容两种字符串类型,减少类型转换和内存分配。如果传入的是明文字符串const char*,const std::string&需要进行一次内存分配,将字符串拷贝到堆上,而std::string_view则可以避免。

  2. 在处理子串时,std::string::substr也需要进行拷贝和分配内存,而std::string_view::substr则不需要,在处理大文件解析时,性能优势非常明显。

// from https://stackoverflow.com/a/40129046
// author: Pavel Davydov

// string_view的remove_prefix比const std::string&的快了15倍
string remove_prefix(const string &str) {
return str.substr(3);
}
string_view remove_prefix(string_view str) {
str.remove_prefix(3);
return str;
}

static void BM_remove_prefix_string(benchmark::State& state) {
std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
while (state.KeepRunning()) {
auto res = remove_prefix(example);
// auto res = remove_prefix(string_view(example)); for string_view
if (res != "aghdfgsghasfasg3423rfgasdg") {
throw std::runtime_error("bad op");
}
}
}

在向std::map/unordered_map中插入元素时,我们往往使用emplaceemplace的操作是如果元素 key 不存在,则插入该元素,否则不插入。但是在元素已存在时,emplace仍会构造一次待插入的元素,在判断不需要插入后,立即将该元素析构,因此进行了一次多余构造和析构操作。c++17 加入了try_emplace,避免了这个问题。同时 try_emplace 在参数列表中将 key 和 value 分开,因此进行原地构造的语法比emplace更加简洁

std::map m;
// emplace的原地构造需要使用std::piecewise_construct,因为是直接插入std::pair
m.emplace(std::piecewise_construct,
std::forward_as_tuple("c"),
std::forward_as_tuple(10, 'c'));

// try_emplace可以直接原地构造,因为参数列表中key和value是分开的
m.try_emplace("c", 10, 'c')

同时,c++17 还给std::map/unordered_map加入了insert_or_assign函数,可以更方便地实现插入或修改语义。

类型系统

c++17 进一步完备了 c++的类型系统,终于加入了众望所归的类型擦除容器()和代数数据类型()

std::any是一个可以存储任何可拷贝类型的容器,C 语言中通常使用void*实现类似的功能,与void*相比,std::any具有两点优势:

  1. std::any更安全:在类型 T 被转换成void*时,T 的类型信息就已经丢失了,在转换回具体类型时程序无法判断当前的void*的类型是否真的是 T,容易带来安全隐患。而std::any会存储类型信息,std::any_cast是一个安全的类型转换。

  2. std::any管理了对象的生命周期,在std::any析构时,会将存储的对象析构,而void*则需要手动管理内存。

std::any应当很少是程序员的第一选择,在已知类型的情况下,std::optional,std::variant和继承都是比它更高效、更合理的选择。只有当对类型完全未知的情况下,才应当使用std::any,比如动态类型文本的解析或者业务逻辑的中间层信息传递。

std::optional代表一个可能存在的 T 值,对应 Haskell 中的Maybe和 Rust/OCaml 中的option,实际上是一种。常用于可能失败的函数的返回值中,比如工厂函数。在 C++17 之前,往往使用T*作为返回值,如果为nullptr则代表函数失败,否则T*指向了真正的返回值。但是这种写法模糊了所有权,函数的调用方无法确定是否应该接管T*的内存管理,而且T*可能为空的假设,如果忘记检查则会有 SegFault 的风险。

// pre c++17
ReturnType* func(const std::string& in) {
ReturnType* ret = new ReturnType;
if (in.size() == 0)
return nullptr;
return ret;

// c++17 更安全和直观
std::optional func(const string& in) {
ReturnType ret;
if (in.size() == 0)
return nullopt;
// ...
return ret;
}

std::variant代表一个多类型的容器,容器中的值是制定类型的一种,是通用的 Sum Type,对应 Rust 的enum。是一种类型安全的union,所以也叫做tagged union。与union相比有两点优势:

  1. 可以存储复杂类型,而 union 只能直接存储基础的 POD 类型,对于如std::vectorstd::string就等复杂类型则需要用户手动管理内存。

  2. 类型安全,variant 存储了内部的类型信息,所以可以进行安全的类型转换,c++17 之前往往通过union+enum来实现相同功能。

通过使用std::variant,用户可以实现类似 Rust 的std::result,即在函数执行成功时返回结果,在失败时返回错误信息,上文的例子则可以改成:

std::variant func(const string& in) {
ReturnType ret;
if (in.size() == 0)
return Err{"input is empty"};
return {ret};

需要注意的是,c++17 只提供了一个库级别的 variant 实现,没有对应的机制,而最接近的std::visit又缺少编译器的优化支持,所以在 c++17 中std::variant并不好用,跟 Rust 和函数式语言中出神入化的 Sum Type 还相去甚远,但是已经有许多围绕std::variant的提案被提交给 c++委员会探讨,包括模式匹配,std::expected等等。

总结一下,c++17 新增的三种类型给 c++带来了更现代更安全的类型系统,它们对应的使用场景是:

  • std::any适用于之前使用void*作为通用类型的场景。

  • std::optional适用于之前使用nullptr代表失败状态的场景。

  • std::variant适用于之前使用union的场景。

总结

以上是笔者在生产环境中最常用的 c++17 特性,除了本文描述的十个特性外,c++17 还添加了如, , 等非常易用的特性,本文篇幅有限不做赘述,欢迎有兴趣的读者自行探索。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
女人G潮时,“喷”出的是水吗?

女人G潮时,“喷”出的是水吗?

水白头
2024-06-16 14:23:11
何谓“法律面前人人平等”?美国司法界判拜登儿子和特朗普都有罪

何谓“法律面前人人平等”?美国司法界判拜登儿子和特朗普都有罪

爆角追踪
2024-06-15 22:57:15
茅台的下跌,也许是贵州的麻烦才开始!

茅台的下跌,也许是贵州的麻烦才开始!

股海风云大作手
2024-06-16 15:33:25
活久见!广东一货车撞上复兴号,保险公司在角落瑟瑟发抖

活久见!广东一货车撞上复兴号,保险公司在角落瑟瑟发抖

起喜电影
2024-06-16 17:13:19
没有经过任何修复的一张毛主席照片,原汁原味的穿着就是这样

没有经过任何修复的一张毛主席照片,原汁原味的穿着就是这样

大江
2024-06-16 13:00:53
甘肃天水烟草局爆大狗,起早贪黑式造假揭秘!

甘肃天水烟草局爆大狗,起早贪黑式造假揭秘!

新动察
2024-06-16 11:36:42
女子找医生通奸,还借给医生90万,2009年被医生截肢成180块抛尸

女子找医生通奸,还借给医生90万,2009年被医生截肢成180块抛尸

汉史趣闻
2024-06-13 09:07:50
土地卖不动以后,为了保障体制内的工资,许多地方开始“大甩卖”

土地卖不动以后,为了保障体制内的工资,许多地方开始“大甩卖”

庞明说财经
2024-06-16 17:00:58
太突然了!山东菏泽曹县翰林府长达7分29秒视频,为何引发热搜?

太突然了!山东菏泽曹县翰林府长达7分29秒视频,为何引发热搜?

火山诗话
2024-06-16 16:42:18
全跑光了,一家7口套现20亿,把公司卖给了17万股民

全跑光了,一家7口套现20亿,把公司卖给了17万股民

灰鸽观察室
2024-06-15 20:09:36
首次!大批俄军被包围,“正处于投降的边缘”

首次!大批俄军被包围,“正处于投降的边缘”

近距离
2024-06-16 13:12:55
失望!瑞士和平峰会开幕,传言不参加的国家都来了

失望!瑞士和平峰会开幕,传言不参加的国家都来了

手工制作阿歼
2024-06-16 17:14:19
辛酸!队友绝杀之后拜合拉木迅速捡球 这次他没敢庆祝

辛酸!队友绝杀之后拜合拉木迅速捡球 这次他没敢庆祝

球事百科吖
2024-06-16 12:02:51
欧洲杯被中文广告包围?破案了:均为虚拟广告牌,与现场不一样!

欧洲杯被中文广告包围?破案了:均为虚拟广告牌,与现场不一样!

风过乡
2024-06-15 16:53:50
马来西亚总理不但看破,还非要说破:我不担心台海,原因很简单

马来西亚总理不但看破,还非要说破:我不担心台海,原因很简单

千里持剑
2024-06-16 12:42:12
6月16日俄乌最新:铁锅炖大鹅

6月16日俄乌最新:铁锅炖大鹅

西楼饮月
2024-06-16 16:13:50
网友:孙兴慜到底说了啥?王大雷调侃回复:他说他对不起中国人民

网友:孙兴慜到底说了啥?王大雷调侃回复:他说他对不起中国人民

直播吧
2024-06-16 12:48:07
姜萍圆梦大学不止职教高考,江苏教育主管部门回应

姜萍圆梦大学不止职教高考,江苏教育主管部门回应

齐鲁壹点
2024-06-16 14:58:15
一家三口因没系安全带遭交警殴打,71岁老人肋骨被打断,官方回应

一家三口因没系安全带遭交警殴打,71岁老人肋骨被打断,官方回应

洛洛女巫
2024-06-16 11:41:32
土耳其女排2-3中国女排,赛后主帅桑塔雷利说了几句肺腑之言

土耳其女排2-3中国女排,赛后主帅桑塔雷利说了几句肺腑之言

百里无心
2024-06-16 10:32:36
2024-06-16 20:10:44
腾讯技术工程
腾讯技术工程
不止于技术
1130文章数 598关注度
往期回顾 全部

科技要闻

iPhone 16会杀死大模型APP吗?

头条要闻

凯特王妃患癌后首次露面 小王子跳舞抢镜被姐姐制止

头条要闻

凯特王妃患癌后首次露面 小王子跳舞抢镜被姐姐制止

体育要闻

没人永远年轻 但青春如此无敌还是离谱了些

娱乐要闻

上影节红毯:倪妮好松弛,娜扎吸睛

财经要闻

打断妻子多根肋骨 上市公司创始人被公诉

汽车要闻

售17.68万-21.68万元 极狐阿尔法S5正式上市

态度原创

艺术
家居
亲子
房产
手机

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

家居要闻

空谷来音 朴素留白的侘寂之美

亲子要闻

原来你是这样的爸爸!

房产要闻

万华对面!海口今年首宗超百亩宅地,重磅挂出!

手机要闻

iOS18来了,苹果的新一轮降价也来了!赶紧上车!

无障碍浏览 进入关怀版