文章目录
- 一、语言核心特性
- 1. 泛型 Lambda
- 2. Lambda 初始化捕获
- 3. 普通函数 auto 返回值推导 + decltype(auto)
- 4. 大幅放宽 constexpr 约束
- 5. 变量模板
- 二、标准库高频新特性
- 1. std::make_unique
- 2. std::exchange
- 和 atomic_exchange 区分开
一、语言核心特性
1. 泛型 Lambda
【C++11 痛点】
- C++11 Lambda 参数必须写具体类型,无法通用。想要适配多种类型,只能写模板函数、重载函数,比较繁琐。
【C++14 新能力】
- Lambda 参数直接用 auto,编译器自动生成模板版 Lambda,天然支持多类型复用,等价于模板函数。
#include<iostream>#include<string>intmain(){// 泛型 Lambda:任意支持+运算的类型都能用autoadd=[](autoa,autob){returna+b;};std::cout<<add(1,2)<<std::endl;// intstd::cout<<add(1.5,2.5)<<std::endl;// doublestd::cout<<add(std::string("a"),"b")<<std::endl;// stringreturn0;}【真实开发场景】
- STL 算法通用回调:
std::for_each、std::transform无需为不同容器写多套回调; - 通用工具回调、事件封装,大幅减少模板函数冗余代码;
【坑点】
- auto 只会推导静态类型,不会隐式转换;两个参数类型不匹配且无法兼容时,编译报错。
2. Lambda 初始化捕获
【C++11 致命短板】
- C++11 Lambda 捕获只有:值捕获、引用捕获;
- 无法捕获只可移动、不可拷贝对象,比如
std::unique_ptr、std::thread,直接值捕获会编译失败;
【C++14 新能力】
- 捕获列表中支持变量初始化,可以:
- 用
std::move将外部对象移动进 Lambda; - 在捕获区临时创建新变量,仅 Lambda 内部使用;
- 用
#include<iostream>#include<memory>#include<functional>intmain(){// 不可拷贝,只可移动autoptr=std::make_unique<int>(100);// C++11 无法实现!C++14 移动捕获autofunc=[p=std::move(ptr)](){std::cout<<*p<<std::endl;};func();// ptr 已经被移动,外部失效return0;}捕获区临时造变量:
autofunc=[val=10+20](){returnval;};【开发场景】
- 异步回调、线程任务捕获独占式资源(unique_ptr、socket 句柄等);
- 避免外部变量污染,Lambda 私有初始化变量;
3. 普通函数 auto 返回值推导 + decltype(auto)
【C++11 限制】
- 仅 Lambda 支持省略返回值类型,普通函数必须写返回值,复杂返回场景需要繁琐的
decltype尾置推导。
【C++14 新能力】
- 普通函数直接用
auto自动推导返回类型,无需手写。同时新增decltype(auto),可原样保留表达式的类型(包括引用)。
// C++14 合法,自动推导返回值为 intautosquare(intx){returnx*x;}// 多返回分支要求类型严格一致automax_val(inta,intb){if(a>b)returna;returnb;// 必须与上面 return 类型一致}【关键区别:auto vs decltype(auto)】
auto:返回值总是值类型(会退化引用和 const);decltype(auto):精确推导,保留表达式的完整类型(值/左值引用/右值引用);
如何正确保留引用?
#include<iostream>intg_val=10;// auto 返回 int(值类型)autoget_val(){returng_val;}// decltype(auto) 不加括号 → 返回 int(不是引用!)decltype(auto)get_val_wrong(){returng_val;// decltype(g_val) → int}// decltype(auto) 加括号 → 返回 int&(左值引用)decltype(auto)get_val_ref(){return(g_val);// decltype((g_val)) → int&}intmain(){get_val()=20;// 编译报错:不能给右值赋值get_val_wrong()=20;// 编译报错:返回的是 intget_val_ref()=20;// 编译通过:g_val 被修改为 20return0;}- decltype(auto) 的括号陷阱:
decltype(auto)foo(){intx=0;return(x);// 返回 int&(悬挂引用!未定义行为!)}- 函数所有 return 语句类型必须完全一致,否则编译失败;
- 递归函数不能直接 auto 推导(编译器无法前置推导);
4. 大幅放宽 constexpr 约束
【C++11 反人类限制】
- C++11 的 constexpr 函数极度鸡肋:函数体只能有一条 return 语句,不能有局部变量、if、for、循环,几乎写不了复杂逻辑。
【C++14 史诗级放开】
- constexpr 函数允许:局部变量、for/while 循环、if 分支、多重语句。可以写完整的业务逻辑,编译期直接算出结果,0 运行时开销。
constexprintcalc_sum(intn){intres=0;// C++11 非法,C++14 合法for(inti=1;i<=n;++i){res+=i;}returnres;}// 编译期直接计算完成,运行时无计算开销constexprints=calc_sum(100);【开发场景】
- 配置常量、数学计算、哈希映射、参数校验放到编译期
- 零开销常量工具函数,替代宏定义(类型安全、可调试)
【 constexpr 函数的"双栖"特性 】
constexpr 函数是编译期和运行时双栖的。
| 调用方式 | 执行时机 | 要求 |
|---|---|---|
constexpr int s = calc_sum(100); | 编译期 | 参数必须是编译期常量 |
int s = calc_sum(n); | 运行时 | 参数可以是运行时变量 |
- 编译器策略:
- 如果所有参数都是编译期常量,且结果赋给 constexpr 变量 → 编译期求值;
- 如果参数有运行时变量 → 退化为普通函数,运行时求值;
- 如果结果没被使用 → 可能被优化掉;
5. 变量模板
【C++11 限制】
- 模板只能用于类、函数,不能给变量做模板,通用常量需要封装函数实现。
【C++14 新能力】
- 支持变量模板,为不同类型提供统一常量模板。
template<typenameT>constexprT PI=T(3.1415926535);// 不同类型复用同一常量模板floatpi_f=PI<float>;doublepi_d=PI<double>;业务代码用得少,底层库、常量工具类常用。
二、标准库高频新特性
1. std::make_unique
【C++11 历史遗留问题】
- C++11 只有 make_shared,没有 make_unique,创建 unique_ptr 只能裸 new,存在内存泄漏风险、代码冗余。
【裸 new 的隐患】
// 不安全!new 后若中间代码抛异常,可能泄漏std::unique_ptr<int>p(newint(10));【C++14 标准写法】
// 异常安全、简洁、无需手写 newautop=std::make_unique<int>(10);// 数组版本autoarr=std::make_unique<int[]>(10);【开发铁律】
现代 C++:创建 unique_ptr 一律用 make_unique,禁止裸 new,和 make_shared 保持统一编码规范。
2. std::exchange
【作用】
- 状态替换神器,头文件
<utility>,用新值覆盖变量,返回旧值,一行代码完成保存旧值 + 更新新值。
【原生写法 vs 标准库写法】
// 传统写法:三行代码,易出错intx=10;intold=x;x=20;// C++14 一行搞定intold_val=std::exchange(x,20);【高频场景】
- 状态机状态切换、资源重置;
- 移动语义资源替换、缓存更新、状态替换;
和 atomic_exchange 区分开
std::exchange不是原子操作,和std::atomic_exchange完全是两回事。
它只是一个高效、便捷的非原子工具函数,专门用来处理普通变量的值替换。
它的底层原理其实就是一个读-改-写的封装,标准库中大概实现是这样的:
template<classT,classU=T>Texchange(T&obj,U&&new_value){T old_value=std::move(obj);// 1. 将 obj 的旧值“移动”出来,存入 old_valueobj=std::forward<U>(new_value);// 2. 将 new_value “完美转发”并赋值给 objreturnold_value;// 3. 返回旧值}这三步是顺序执行的,没有加锁,也没有任何同步机制。在多线程环境下,如果多个线程同时操作同一个变量,就会出现数据竞争。
在 C++ 标准库中,真正提供原子“交换”操作的是std::atomic<T>::exchange(),它在<atomic>头文件中定义,用于多线程同步。
当a,b,c都是自定义类型时,auto a = std::exchange(b, c);这行代码背后发生的一系列动作,可以看作是一个精巧的“三步走”流程:
- 移动取出旧值:
T old_value = std::move(b);- 它会尝试调用
b的移动构造函数,将b的资源“窃取”过来,存入一个临时变量old_value中; - 执行完这一步后,
b会处于一个“有效但未指定”的状态,通常被移走后内部资源为空; - 这是实现高效交换的关键一步,避免了不必要的深拷贝;
- 它会尝试调用
- 赋值放入新值:
b = std::forward<U>(c);- 这一步将
c的值赋给b,std::forward<U>保证了c的值类别(左值或右值)被完美转发; - 如果
c是一个左值(比如一个具名变量),这里会调用b的拷贝赋值运算符,复制一份c的数据; - 如果
c是一个右值(比如临时对象或std::move(c)),则会调用b的移动赋值运算符,实现高效的资源转移;
- 这一步将
- 返回旧值:
return old_value;- 函数最后将保存着
b旧值的old_value返回; - 因为
old_value是一个局部变量,返回值时通常会触发移动语义(如果可能)或复制省略( RVO)优化,将旧值高效地“转移”给变量a;
- 函数最后将保存着