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

C++11 到 C++23 新特性实用盘点——哪些真的在用

0
分享至

「我们用的是同一个 C++ 吗?」,这句是 我做研发 20 多年,听的最多一句。

其实 C++11 之前我们用的确实是同一,但 C++11 之后,在准确点,应该是 2012 年起。


C++11 来了,它把 auto、lambda、移动语义、智能指针、线程库塞进来,彻底改变了 C++ 的写法。

从此每三年一个新标准,彻底将 C++ 工程师细化。

本文把水分挤干净,直接告诉你哪些值得死磕,哪些看完就忘。

一、C++11 是原点,吃透了再往下走

C++11 是现代 C++ 的起点,它改变了 C++ 的基本写法。

1、auto

auto 推的是编译期类型,运行时没有任何额外开销,不存在性能问题。

auto x = 42; // int
auto y = 3.14; // double
auto p = std::unique_ptr(newint(5)); // unique_ptr
auto& ref = container; // auto 保留左值引用语义

唯一要小心的是函数签名和类成员变量。

这两处类型名对外部可见,必须写清楚。

其余场景用 auto 能让代码更干净,尤其在遍历 STL 容器和写泛型回调的时候。

一个容易踩的坑:auto 推的是初始化表达式的类型,不是你想当然的类型。

std::vector v;
auto it = v.begin; // std::vector::iterator,正确
auto x = *v.begin; // int,正确


// 陷阱
conststd::vector& ref = v; // 写了 const 声明,ref 是 const vector&
auto& auto_ref = ref; // auto_ref 推导成 const vector&,不是 vector&

当你觉得 auto 推出来的类型不符合预期,问题往往出在变量的职责定义上,不是 auto 的问题。

2、移动语义,搞清楚三件事就够了

左值有稳定地址,右值是临时对象。移动构造函数把别人手里的资源直接抢过来,省掉一次内存分配。这个「抢」的动作发生在对象构造阶段,析构阶段把资源释放掉,不重复。

classBuffer {
int* data_;
public:
Buffer(Buffer&& other) noexcept : data_(other.data_) {
other.data_ = ptr; // 必须置空,否则析构时 double free
}
};

noexcept 不能漏。

标准库容器在 rehash、reserve、扩容的时候,需要把元素从一个地方挪到另一个地方。

如果元素是 noexcept 可移动的,容器选移动;不是的话,容器会退而求其次选拷贝,这是标准库的保底策略。如果你的移动构造函数漏了 noexcept,容器在关键路径上会默默选更慢的拷贝。

完美转发 解决的是参数在层层传递过程中值类别丢失的问题:

template
voidwrapper(T&& arg){
// T&& 在模板推导中是转发引用,不是单纯的右值引用
process(std::forward(arg)); // 保持调用者传入的值类别
}

没有 forward ,所有参数在转发函数里都被当左值处理,移动语义在第一层传递之后就断了。

emplace_back vs push_back

v.emplace_back(Widget{1, 2, 3}); // 参数直接传给 vector 构造函数,原地构造
v.push_back(Widget{1, 2, 3}); // 先构造临时对象,再移动(或拷贝)进去

对有资源管理的类型,优先用 emplace_back。现代编译器能优化返回值构造,但带显式构造函数的有资源类型不一定触发 copy elision,emplace_back 永远少一次临时对象的构造开销。

3、智能指针,搞清楚谁拥有谁

std::unique_ptr 是首选,开销和裸指针完全一样,析构时自动释放。

std::unique_ptr cfg = std::unique_ptr(new Config("config.json"));
// 独占语义,作用域结束自动析构,不需要手动 delete

std::shared_ptr 在需要共享所有权时用,内部靠引用计数。引用计数归零才释放资源。

问题在于循环引用,两个 shared_ptr 互相指向对方,引用计数永远不为零,内存泄漏。

structNode {
std::shared_ptr next;
};
Node a, b;
a.next = std::make_shared;
b.next = std::make_shared;
a.next->next = std::make_shared(b);
b.next->next = std::make_shared(a); // 循环引用,计数永远归不了零

打破循环用 std::weak_ptr ,它不参与计数,只是一个「观察者」。

父节点持有 weak_ptr 指向子节点,子节点持有 shared_ptr 指向父节点,循环就断了。

缓存里用 weak_ptr 也是同一个道理。

对象被外部释放后, weak_ptr 自动失效,不需要手动清理缓存。

4、lambda

lambda 本质是编译器生成的匿名函数对象,写法比 std::bind 直观得多。

std::bind 那种 Placeholder 占位符的写法,现在能用 lambda 解决的就坚决不用 bind。

auto cmp = (const Person& a, const Person& b) { return a.age < b.age; };
std::sort(persons.begin, persons.end, cmp);

捕获列表四种最常用的写法:

[=] // 按值捕获,lambda 里是副本,加 mutable 才能在内部修改
[&] // 按引用捕获,不推荐,生命周期风险极大
[x, &y] // x 按值,y 按引用,明确控制
[this] // 捕获 this 指针,访问成员变量


// C++14 的初始化捕获:在捕获前对变量做变换再拿进来
auto handler = [data = std::move(raw)] mutable { /* 使用移动进来的 data */ };
5、override 和 final

classBase {
virtualvoidrun= 0;
virtualvoidinit{ }
virtual ~Base = default;
};


classDerived : Base {
voidrunoverride{ } // 编译器检查基类是否有 run,没有则报错
voidInitoverride{ } // 编译报错:基类没有 Init,只有 init
voidinitoverride{ } // 对
voiddonefinal{ } // 子类禁止重写 done
};

参数类型差一个 const,编译器不报错,运行起来行为全变。

这种 bug 在大型继承体系里极难定位。

加上 override,编译器在定义阶段就把问题抓出来。

final 同理,告诉编译器此类或此方法禁止派生,编译器可以据此做更激进的内联优化。

6、constexpr

C++11 的 constexpr 只能一条 return,功能极其受限。

C++14 解除了这个限制,for 循环、局部变量、条件分支都能用 constexpr 函数体了。

constexprintfactorial(int n){
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
static_assert(factorial(10) == 3628800, "compile-time check");

编译阶段算出来的值可以作为数组大小、模板参数、 static_assert 的条件。

运行时零开销,不占栈空间。

7、线程,避坑比会用更重要

std::atomic counter{0};
std::vector workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back([&counter] {
for (int j = 0; j < 10000; ++j)
counter.fetch_add(1, std::memory_order_relaxed);
});
}
for (auto& t : workers) t.join;

内存顺序按需选择:

场景

选哪个

单个 atomic 变量做计数,不需要和其他变量同步

memory_order_relaxed

,最快

多个 atomic 之间有 happens-before 关系

memory_order_seq_cst

,最安全

自定义 lock-free 算法

acquire

+ release

最大的坑是捕获局部变量引用。

上面的 [&counter] 没问题是因为 counter 在主线程里存活,线程结束前它一直在。

但如果捕获列表写 [&] 而不是指名道姓,作用域里有个局部变量在某个时刻析构了,线程里拿到的就是悬空引用。

二、C++14,记住三个特性就够了

C++14 被很多人直接跳过,觉得没什么可看的。

实际上值得专门记的只有三个。

1、泛型 lambda

auto 进 lambda 参数,编译器自动生成对应的函数模板:

auto twice = (auto x) { return x * 2; };
twice(5); // int
twice(3.14); // double

STL 算法里写回调,一个 lambda 覆盖所有类型:

std::transform(v.begin, v.end, v.begin,
(auto&& x) { returnstd::forward(x) * 2; });

auto&& 在 lambda 参数里是转发引用的简写,配合 std::forward 可以把值类别原封不动地透传。

2、decltype(auto)

decltype(auto) 是 C++14 新增的组合关键字,让编译器用 decltype 的规则推导类型,而不是用 auto 的规则。

auto 在推导返回类型时会退化掉引用, decltype(auto) 按照 decltype 的精确规则来:

decltype(auto) clone(const Widget& w){ return w; }
// decltype(w) 是 const Widget&,返回 const Widget&,正确


autoclone(Widget w){ return w; }
// 返回 Widget 值类型,调用者收到拷贝,多了一次构造

这条规则在实现转发函数和工厂函数时极其实用。

3、变量模板

template
constexpr T pi = T(3.1415926535897932385L);


float area_f = pi * r * r;
double area_d = pi * r * r;

之前写 MathConstants::pi 需要一个完整的类包装,变量模板把类包装甩掉了。

标准库也跟进了, std::numeric_limits的相关常量在 C++14 里补了变量模板版本。

三、C++17 三个特性,改变最大

C++17 有六十多个特性,大多数没人用,但这三个值得你仔细研究一下。

1、结构化绑定

std::unordered_map m = {{"apple", 1}, {"banana", 2}};
for (auto& [key, value] : m) {
std::cout << key << ": " << value << '\n';
}

auto [min_val, max_val] = std::minmax({3, 1, 4, 1, 5, 9, 2, 6});
auto [it, inserted] = m.insert({"cherry", 3});

structPoint {double x, y, z; };
Point p{1.0, 2.0, 3.0};
auto [a, b, c] = p;

可以解包 array、tuple、pair 以及任何带公开成员的结构体。 auto& 绑定到成员的引用上, auto 绑定到副本上。

坑:结构化绑定声明的是一个新变量,不是给已有变量起别名。如果绑定出来的变量名和作用域里已有变量名重名,两个变量各是各的,编译器不会报错,容易埋下不易察觉的 bug。

std::pair p{1, 2};
auto& [x, y] = p; // x 和 y 是 p.first、p.second 的引用
x = 10; // p.first 被改了,就这么简单,别想复杂了
2、if constexpr

SFINAE 写多了的人都懂那种感觉:模板特化套模板特化,错误信息一屏都是垃圾,调试全靠脑补编译器行为。

// C++17 之前
template>
autoderef(T t) -> decltype(*std::declval){
return *t;
}


// C++17
template
autoget_value(T t){
ifconstexpr(std::is_pointer_v){
return *t;
} else {
return t;
}
}

if constexpr 的核心在于 else 分支在模板实例化阶段根本不进入编译器视线,类型不匹配不影响编译。

这叫编译期条件,是 C++17 最有价值的特性之一。

3、std::optional

「可能没有」从约定变成强制语义:

std::optional find(int id);
auto r = find(42);
if (r) {
use(*r);
} else {
notFound;
}
int value = find(42).value_or(-1); // 有值返回值,无值返回 -1

optional 和异常的边界是,异常表达「不应该发生」,optional 表达「可能没有」。

文件找不到用哪个?取决于业务语义,「配置文件不存在」用默认值就行,用 optional;程序严重依赖某个文件才能运行,缺失是故障,用异常。

optional 的大小和裸指针一样,不适用于大对象。

如果值本身很大,不要用 optional 包装值,而是把 optional 放进容器里:

std::optional> data; // 不推荐,vector 很大,多一次间接
std::vector> data; // 推荐,每个元素独立管理有无状态
四、C++20

C++20 被吹得最厉害,协程、Modules、Concepts、Ranges 四大旗舰特性齐发。但生产环境里稳定好用的,我只推荐三个。

1、std::format

snprintf 的类型安全问题在编译期就堵死了:

#include

std::string s = std::format("Hello {} {}, value={:.2f}", "world", 42, 3.14159);

// 对齐和填充
std::format("{:>10}", "right"); // 右对齐,宽度 10
std::format("{:*std::format("{:^10}", "center"); // 居中

// 进制
std::format("0x{:08x}", 255); // 十六进制零填充
std::format("{:b}", 10); // 二进制
std::format("{:o}", 64); // 八进制

// C++23
std::print("Progress: {:.1f}%\n", 75.5);
std::println("Error: {}", msg);

sprintf("%d", "hello") 在 C 里不报错,运行才崩。 std::format{} 占位符,类型不匹配编译器直接报。

2、std::span

把「裸指针加长度」这对搭档合二为一:

voidprocess(std::span data){
for (int x : data) { /* 直接当容器用 */ }
}


std::vector v{1, 2, 3, 4, 5};
process(v);
process(std::span{v.data, 2}); // 只取前两个元素

span 本质是非拥有型范围,只读场景用 span,要修改用 span

所有接口里出现裸指针加长度的地方,都值得换成 span。

// 之前
voidread_data(constuint8_t* buffer, size_t size);
// 现在
voidread_data(std::span data);
3、std::jthread

jthread 解决了 std::thread 析构时还没 join,程序直接 std::terminate 的问题。

std::jthread worker((std::stop_token st) {
while (!st.stop_requested) {
do_work;
}
});
// 析构时自动 request_stop + join

stop_token 让工作线程可以感知到停止信号,干净利落地退出。

新起线程默认用 jthread ,不要再用裸 std::thread

4、Concepts,读懂标准库就够了

template
T abs_add(T a, T b){ return a >= 0 ? a + b : a - b; }


abs_add(10, 6); // OK
// abs_add(3.14, 2.71); // 编译器报:不满足 std::integral,直接指向调用点

没有 Concepts,模板参数不满足的错误信息从模板深处冒出来,一屏都是垃圾。

有了 Concepts,编译器直接定位到调用点,告诉你哪个类型不满足哪个概念。

普通业务代码不需要自己写 Concepts,标准库已经预定义了大量 concepts。

真正需要的是能读懂标准库头文件里的 requires 子句。

三路比较运算符

auto operator<=>(const Point&) const = default;

一行生成六个比较运算符。

大多数业务类型不需要六个比较关系,等真的要用 std::map时再加。

五、C++23 三件事值得掌握

C++23 是个小版本。

1、std::expected

optional 只能表示「有没有值」,expected 在此基础上把失败原因带出来:

#include


std::expected parse(conststd::string& s){
if (s.empty) returnstd::unexpected(std::errc::invalid_argument);
try {
returnstd::stoi(s);
} catch (...) {
returnstd::unexpected(std::errc::invalid_argument);
}
}
auto r = parse("42");
if (r) {
use(*r);
} else {
handle_error(r.error); // 拿到具体错误码,不是「没值」这个模糊状态
}

错误可能来自多种原因的场景,用 expected 比 optional 加全局错误码清晰得多。

2、std::print / std::println

std::print("Hello {} {}\n", "world", 42);
std::println("Error: {}", msg);
std::print(stderr, "Error: {}: {}\n", code, msg); // 错误流

std::formatcout 的两步,变成一步。

3、std::views::enumerate

for (auto [i, x] : std::views::enumerate(v)) {
std::print("{}: {}\n", i, x);
}

std::ranges::contains

std::find(v.begin, v.end, x) != v.end

换成一句 std::ranges::contains(v, x)

C++23 给 optional 加了 monadic 操作( transformand_thenor_else ),链式处理比手写分支干净:

auto result = parseInt("42")
.transform((int x) { return x * 2; }) // 有值 → 变换
.and_then((int x) { // 有值 → 做检查,失败返回 opt
return (x > 0) ? std::optional(x) : std::opt;
})
.value_or(-1);
六、总结

现代 C++ 必须掌握的

C++11:auto、范围 for、lambda、移动语义、智能指针、override/final、constexpr、线程基础

C++14:泛型 lambda、decltype(auto)、变量模板

C++17:结构化绑定、if constexpr、std::optional、std::string_view、[[nodiscard]]

C++20:std::format、std::span、std::jthread、Concepts(会读标准库即可)

C++23:std::expected、std::print、std::views::enumerate、std::ranges::contains、monadic optional

以上加在一起,不到二十个,覆盖了日常代码 90% 的场景,其他的知识点用到再查就好。

C++不是一个需要追着它跑的语言。把上面那不到二十个核心特性啃透了,比把两百多个特性列表背下来管用。

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

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.

相关推荐
热点推荐
这是空调正确的打开方法,牢记这4点,凉快又省电

这是空调正确的打开方法,牢记这4点,凉快又省电

小柱解说游戏
2026-07-14 10:08:11
“我对他没有一点母爱了!”妈妈直言厌恶3岁儿子,网友一反常态

“我对他没有一点母爱了!”妈妈直言厌恶3岁儿子,网友一反常态

妍妍教育日记
2026-07-10 08:45:08
反诈紧急提醒:马上关闭该权限,守住银行卡里的钱!

反诈紧急提醒:马上关闭该权限,守住银行卡里的钱!

娱乐圈见解说
2026-07-14 19:57:14
168万拍下“乾隆梅瓶”仅值250元!拍卖公司经理当托儿诈骗获刑 受害人:一共卖了我5个亿

168万拍下“乾隆梅瓶”仅值250元!拍卖公司经理当托儿诈骗获刑 受害人:一共卖了我5个亿

封面新闻
2026-07-14 23:30:11
又一个乌克兰!全面去俄化,不准再叫将军,只能叫突厥军衔

又一个乌克兰!全面去俄化,不准再叫将军,只能叫突厥军衔

乐享人生风雨
2026-07-15 08:56:51
免去武汉市副市长职务,李湛新职明确

免去武汉市副市长职务,李湛新职明确

一口娱乐
2026-07-15 14:02:15
83年飞行员王学成叛逃台湾,邓丽君慰问时耳语一句后被强行支走

83年飞行员王学成叛逃台湾,邓丽君慰问时耳语一句后被强行支走

鉴史录
2026-05-24 15:48:49
中国籍女医生在柬遇害现场曝光,嫌疑人姐姐:弟弟有精神类疾病,此前杀害亲妹刚被释放

中国籍女医生在柬遇害现场曝光,嫌疑人姐姐:弟弟有精神类疾病,此前杀害亲妹刚被释放

红星新闻
2026-07-14 14:45:16
心理学:别人夸你“看起来真年轻”,千万别答“哪有哪有”,机智的人只需一句话就能让对方更愉快

心理学:别人夸你“看起来真年轻”,千万别答“哪有哪有”,机智的人只需一句话就能让对方更愉快

心理观察局
2026-07-01 08:31:08
蒋方舟抄袭事件反转!胡锡进:我们要支持人大“纠错”、拿下蒋方舟论文的这份勇气

蒋方舟抄袭事件反转!胡锡进:我们要支持人大“纠错”、拿下蒋方舟论文的这份勇气

火山詩话
2026-07-14 07:04:14
第二个许家印!又一首富栽了!世界500强竟是假的,千亿帝国清零

第二个许家印!又一首富栽了!世界500强竟是假的,千亿帝国清零

阿绐聊社会
2026-07-07 05:55:20
马斯克宣布X平台将全量开源代码库,邀请第三方核验

马斯克宣布X平台将全量开源代码库,邀请第三方核验

IT之家
2026-07-15 22:02:10
广州增城下调多个小区物业费 广州院子降至2.8元/平方米

广州增城下调多个小区物业费 广州院子降至2.8元/平方米

观点机构
2026-07-15 18:33:15
人老了,千万别太注重打扮了,不是迷信,而是过来人说的大实话

人老了,千万别太注重打扮了,不是迷信,而是过来人说的大实话

白宸侃片
2026-07-15 10:56:17
害羞的超短裙,勉强藏住了一点点紧致

害羞的超短裙,勉强藏住了一点点紧致

梅梅聊点实尚嗑
2026-07-12 08:55:11
禁令刚满96小时,中方准时参战,全球氦气大战爆发,日本第一个输

禁令刚满96小时,中方准时参战,全球氦气大战爆发,日本第一个输

铁锤侃侃而谈
2026-07-15 12:28:03
1947年大别山四百多伤兵遭地主武装杀害,肖永银怒火中烧铁腕复仇

1947年大别山四百多伤兵遭地主武装杀害,肖永银怒火中烧铁腕复仇

墨策讲历史
2026-07-13 23:20:20
警钟:月嫂、月子中心托管,正在悄悄毁掉孩子,让安全感缺失!

警钟:月嫂、月子中心托管,正在悄悄毁掉孩子,让安全感缺失!

小武侃风云
2026-06-28 07:47:51
昆凌晒11岁大女儿生日现场,与周杰伦看完温网比赛立刻返程为女庆生

昆凌晒11岁大女儿生日现场,与周杰伦看完温网比赛立刻返程为女庆生

露珠聊影视
2026-07-15 18:05:25
阿根廷和英格兰迎来第6次对决,世界杯史上最多交手次数是7次

阿根廷和英格兰迎来第6次对决,世界杯史上最多交手次数是7次

懂球帝
2026-07-16 02:08:20
2026-07-16 02:59:00
呼呼历史论
呼呼历史论
分享有趣的历史
665文章数 17521关注度
往期回顾 全部

科技要闻

国行大突破!“Apple智能”已备案

头条要闻

美国要“彻底瓦解”国际刑事法院 日本慌了

头条要闻

美国要“彻底瓦解”国际刑事法院 日本慌了

体育要闻

世界杯两大巨星,加一起22岁

娱乐要闻

大S遗嘱曝光!S家拒不承认

财经要闻

梁文锋身家2400亿登顶全球AI首富

汽车要闻

爱玩会玩 小鹏MONA L03这次来势凶猛

态度原创

时尚
房产
亲子
游戏
军事航空

全世界最会 “赘” 的男人,身价疯涨

房产要闻

海南买房,开始大规模返现了!

亲子要闻

破解矮小症性早熟难题,深圳市儿童医院牵头成立全国诊疗联盟

FS社《The Duskbloods》测试官宣!7月22日报名

军事要闻

美军称已恢复对伊朗的海上封锁

无障碍浏览 进入关怀版