C++历史概述
C++是一门历史悠久的语言,原来叫C with classes,自1979年以来已经出现了98/03/11/14/17五个标准,因此C++中的Workaround和Best practice是随着标准演化而推进的。因此这里首先概览一下C++从开始设计以来的特性的引入、废止与更新的过程,以及对C++11及以后引入的重要特性的一些论述。
史前时代
1979年,这门语言基于C(cfront将C++编译到C)实现了对象机制(但还没有虚函数等运行时多态),inline函数(大快人心的大好事)等。1985年,cfront正式面世,现在虚函数、重载、引用、new/delete、const这些我们常见的概念都出现了,而标准库也有了雏形。1989年是另一个重要的年头,多继承、抽象类、static/const成员函数、类限定的指针(指向类成员)/new/delete应运而生,C++的OOP机制进一步增强。
1998年,C++迎来第一个ISO标准。98标准列出了RTTI机制、virtual(cppreference上翻译成了协变返回类型)、各种cast以及自定义的operator T
、mutable/bool关键字和模板的一些特性。
2003年,C++03发布了,这是一个次要版本,之后C++0x标准的出台陷入了长久的等待。在这段时间中C++标准库引入了tr和tr1两个扩展包(主要来自Boost和C99的冷饭)。
C++11
2011年,C++11千呼万唤始出来。C++11带来的主要新特性如下,其中斜体表示标准库特性:
auto
/decltype
、尾随返回类型(trailing)1
2
3
4template<typename T, typename U>
auto add(T x, U y) -> decltype(x + y){
return x + y;
}decltype
和完美转发能够非常方便地替换result_type
、argument_type
之类的手动实现,于是在C++17中,这些机制被去除了。与之同时被去除的还有result_of
现在变为invoke_result
右值/移动语义/完美转发
详见我的文章C++右值enum class
=delete
和=default
final
/override
constexpr
user defined literals
这里指可以支持operator""
了列表初始化、委托/继承构造函数、花括号初始化器
详见我的文章C++初始化方式nullptr
/nullptr_t
/long long
/char16_t
/char32_t
等类型类型别名(别名模板)
现在我们可以用using
替代typedef
了1
2template <typename T> using V = vector<T>;
using VI = vector<int>;可变参数模板(参数包)
详见我的文章C++模板编程放松了union的限制
放松了POD的限制,现在分为trivial和standard_layout
详见C++初始化方式和我的其他文章Unicode字面量
用户自定义字面量和
operator ""
现在支持下面这些很骚的写法1
212_km
0.5_Pa属性
属性用两个中括号括起来,如[[ noreturn ]]
,用来标准化像__attribute__
、__declspec
这类的用法lambda表达式
noexcept
和新的异常处理机制alignof
和alignas
详见我的文章C++内存对齐与多态thread_local
、原子库和线程库
包括<thread>
、<conditional_variable>
、<mutex>
、<future>
、<atomic>
等头文件。
详见我的文章并发编程重要概念及比较GC的API
range for
1
2
3
4int ran[3] = {1,2,3};
for(int & x: ran){
...
}static assert
emplace_back
C++11的完美转发和参数包特性支持了emplace_back
的实现,我们避免了构造并复制/移动临时对象的开销。std::initializer_list
std::forward_list
chrono库
ratio库
algorithm库
引入了新的算法,如all_of
系列、is_permutation
系列、copy_
系列、move
系列(注意不是右值引用的那个move)、shuffle
、is_partioned
系列、is_sorted
系列、is_heap
系列。新的allocator:
std::scoped_allocator_adaptor
合并tr1
tr1主要包含了reference_wrapper
、智能指针、mem_fn
(之前从来没用过)、result_of
、std::function
和std::bind
、各种trait函数、随机数模块、新的数学函数、tuple
、array
、regex、unordered_
系列容器。在exception库中合并Boost的部分异常处理机制
exception_ptr
、error_code
、error_condition
合并Boost的迭代器
std::begin
、std::end
、std::next
、std::prev
标准库新增容器
unordered_map
、unordered_set
、tuple
、array
(从tr1中引入)string库
增加了std::to_string
等函数
C++14
lambda
C++14放松了lambda的要求,现在lambda的参数可以使用auto
声明了1
2
3
4// C++11
auto l = [](int x){return x;};
// C++14
auto l = [](auto x){return x;};此外,在C++11中lambda表达式是无法捕获右值得到,于是有了诸如Lambda如何捕获转换自临时变量的右值这样的问题。
现在lambda允许捕获右值了,这是典型的给C++11擦屁股1
2
3
4
5Value v = ...;
// C++14
auto l = [value = std::move(v)] {return *value;};
// C++11
auto l = std::bind([] (const Value & value){return *value;}, std::move(v));增强了类型推导(函数返回值)
对于普通函数,我们现在可以进行如下声明而免去尾随返回类型(trailing)了。但是我们要注意对于有多个return的情形,我们要保证所有的返回值推导到一致的类型,对于函数中出现递归调用的情况,需要在当前行前看到至少一个return定义。如下面的例子中语句2涉及递归调用,那么在它前面必须要出现一个能够推导的return(语句1),如果我们颠倒语句1和2的出现次序,编译将报错。1
2
3
4
5
6
7
8
9auto fac(int i){
if(i == 0){
// 1
return 1;
}else{
// 2
return fac(i - 1) * i;
}
}增强了类型推导(
decltype(auto)
)
C++11引入了两种类型推导方式auto
和decltype
。其中auto
始终推导出一个非引用的类型,如同std::decay
所做的那样,而auto &&
始终推导出一个引用类型。decltype
的推导结果则和具体表达式密切相关,例如decltype(*ptr)
是带引用的。WIKI上列出了一些demo,我们可以看到decltype区分值和表达式,对于表达式始终是返回引用的。1
2
3
4
5
6
7
8int i;
int&& f();
auto x3a = i; // x3a的类型是int
decltype(i) x3d = i; // x3d的类型是int
auto x4a = (i); // x4a的类型是int
decltype((i)) x4d = (i); // x4d的类型是int&
auto x5a = f(); // x5a的类型是int
decltype(f()) x5d = f(); // x5d的类型是int&&C++14通过
decltype(auto)
为auto
声明提供了decltype
的行为。放松了
constexpr
根据文章,这一些列变化主要体现在放松了对constexpr函数体中的限制模板变量
放松了聚合初始化的要求
C++11允许使用default member initializer初始化构造函数没有初始化的成员。C++14允许在聚合初始化中使用这个特性,下面的代码现在成为可能1
2
3
4
5struct CXX14_aggregate {
int x;
int y = 42;
};
CXX14_aggregate a = {1}; // C++14允许。a.y被初始化为420b
引导的2进制数字常量、数字分位符(便于阅读)线程库和原子库
引入<shared_mutex>
读写锁的相关实现shared_timed_mutex
、shared_lock
(注意shared_mutex
从C++17开始提供)。
shared_lock
配合shared_mutex
可以实现读锁。标准库提供了一系列
_t
函数用来简化提取traits书写标准库提供了一系列自定义literal来简化书写
这个特性能够实现例如chrono库中的“时间单位”,如
s、
h、
ms、
us等。这个是通过String Literal Operator,即
operator””实现的。特别需要注意的是,由于这项特性的引入,连接字符串字面量和变量的时候必须要在中间空一格空格。例如1
2
3
4// Not OK
"%"PRId64
// OK
"%" PRId64关联容器支持异构查找
标准库的其他更新
tuple
允许通过类型索引(但必须类型在tuple中是唯一的),看起来并没有什么用- 引入了
std::make_unique
给C++11擦屁股 - 整数序列
std::integer_sequence
- 增强了traits(擦屁股)
std::integral_constant
的const的operator()
std::is_final
std::exchange
函数- 增加了全局的
std::cbegin
/std::cend
(又是擦屁股) std::quoted
函数用来处理IO
C++17
static_assert
现在可以省略第二个参数了(这居然也算特性)Class template argument deduction (CTAD)
template template现在允许
typename
了,之前只能用class
他这个typename
啦class
啦啥的总是有很多人搞不清,上次用那个RapidXML,多加了几个typename
类型推导
现在auto可以从braced-init-list推导了,即我们可以按照下面的写法1
2
3
4
5auto x1 = { 1, 2 }; // decltype(x1) is std::initializer_list<int>
auto x2 = { 1, 2.0 }; // error: cannot deduce element type
auto x3{ 1, 2 }; // error: not a single element
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) is intnamespace可以用
::
连接了Allowing attributes for namespaces and enumerators
新的attributes
[[fallthrough]]
[[maybe_unused]]
[[nodiscard]]
。一个 Discarded value expression表示我们执行它只是为了它的副作用而不是结果。如果我们不使用返回的结果,或者不static_cast<void>(f)
(称为 cast to void)时,会产生一个警告提供了
u8
来表示UTF-8编码的char,这是出于兼容性和擦屁股考虑的,因为仍然只有一个字节,所以只能放ASCII二进制的浮点表示。。。
允许所有非类型模板参数的常量计算(?)
Fold expr折叠表达式。。。让你的C++代码越发令人望而生畏
常量表达式
if constexpr
结构绑定(structured binding)
这个是非常有用的特性了,可以用来做pattern matching。可以用它实现聚合类的反射。if
和switch
里面也可以定义变量了,这个特性也不错copy-initialization and direct-initialization of objects of type T from prvalue expressions of type T (ignoring top-level cv-qualifiers) shall result in no copy or move constructors from the prvalue expression. 这一段话总而言之就是guaranteed copy elision的一些规则,可参考我的问题。
Some extensions on over-aligned memory allocation
构造函数可以模板推导了
下面的代码在C++17是可行的。这个之前在VS2015上写CFortranTranslator的时候就想有,上SoF查了一下发现原来真有,可惜是C++17,所以只能函数模板封装一层,做成make_
系函数,估计C++20之后标准库里面这些make_
函数都要deprecate吧。1
2
3
4// C++17
std::pair(5.0, false);
// C++14
std::pair<double,bool>(5.0, false);Inline variable
在我的博文中指出了这个特性的作用,以及没有这个特性之前的丑陋的Workaround__has_include
标准库新增一些库包括
std::string_view
、std::optional
、std::any
、std::variant
(涉及到合并了一些TS的特性)
这四个库是很有用的std::byte
int std::uncaught_exceptions() noexcept
替换了bool std::uncaught_exception() noexcept
,所以我们不只学会了英语的复数容器
map系容器添加了两个方法:extract 和 merge。
extract 通常被用来修改一个 map 的 key,但避免 reallocation1
2
3
4
5std::map<int, std::string> m{{1, "mango"}, {2, "papaya"}, {3, "guava"}};
auto nh = m.extract(2);
nh.key() = 4;
m.insert(std::move(nh));
// m == {{1, "mango"}, {3, "guava"}, {4, "papaya"}}merge 用来将接受的参数中的 key 插入到自己之中(忽略重复的 key)。
增加了统一的std::size
、std::empty
、std::data
访问容器Definition of “contiguous iterators”
一个 LegacyContiguousIterator 表示逻辑相邻的元素在物理上也是相邻的。基于Boost的一个文件系统库
STL算法的并行版本
新的数学函数
现在有椭圆积分和贝塞尔曲线了逻辑运算
std::conjunction
、std::disjunction
、std::negation
std::scoped_lock
可以看做是 lock_guard 的超集。相比 lock_guard 能接受0/1个 mutex,scoped_lock 可以接受1-n个 mutex。1
2
3
4
5
6
7
8
9
10
11std::scoped_lock lock(e1.m, e2.m);
// Equivalent code 1 (using std::lock and std::lock_guard)
// std::lock(e1.m, e2.m);
// std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
// std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
// Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
// std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
// std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
// std::lock(lk1, lk2);
C++20
限制类型特性
concepts前
在concepts前,C++限制类型特性常可以通过static_assert
对应traits或直接上SFINAE解决,这里还列出一些特殊的情形。
- 创建只能在栈上的对象
在对象内重载void * operator new (size_t)
- 创建只能在堆上的对象
禁用析构函数 - 在栈上new对象
使用placement new - 不借助
final
关键字创建final对象
实际上就是让我们不能定义出一个派生对象,我们知道将构造函数设为私有之后这个这个类就不能实例化了,不过这个就像化疗一样,虽然派生类不能实例化了,但是自己也不能实例化。 - 将函数的返回值加入重载决议
注意返回值不是函数签名的一部分(所以函数重载决议也是不包括返回值的),不被推导。如果希望实现将返回值也加入重载决议类似的效果,可以借助于类型转换操作符operator T::U()
实现。
concepts后
concepts后的C++发生了翻天覆地的变化,翻身码农把歌唱,过去的地主富农们装逼的套路又少了很多。可惜码农们南望王师又一年,这concepts是迟迟不来啊。
反射
我一直认为C++、反射和优雅之间只能同时存在两个。在C++17标准前,C++实现反射主要有以下的几种思路:
- 手动注册
rttr是一个较为成熟的库,它并不丑,但是免不了需要在程序运行之前手动执行RTTR_REGISTRATION
一下。 - 基于编译中间结果
内存管理
C++在内存管理方面常出现的问题包括如下的方面:
- 缓冲区溢出
由于现行冯洛伊曼架构,这个是老生常谈的话题了,由此还派生出专门的栈/堆溢出攻击。在C++层面,我们需要审慎使用sprintf
或者strcpy
等函数,或者使用能显式指定长度的_s
系函数。在系统层面,有一些常见的方法,例如金丝雀值。 - 内存泄露
假设我们的程序是一个批处理程序,那么内存泄露其实影响有限,如果我们能够确保我们的代码逻辑是没有问题的话。 - double free
double free一个悬空指针产生的问题相对内存泄露的后果要严重多,因为它会导致SegmentFault或者Double free or corruption。但一般来说这样的RE甚至是好事情,double free问题通常意味着代码存在严重的逻辑错误,这时候RE至少能dump,总比WA之后穷查log要好吧。 - 非法访问
非法访问包括访问越界地址和访问未初始化(完毕)或者被销毁的的变量。例如我们删除了某个指针,但没有置指针值为nullptr
,那么现在这个指针就称为悬空指针,对它进行访问会造成Access Violation等错误。
C++中对此还有一些更为隐晦的规则,例如C++中在构造函数和析构函数中不能访问虚函数。或者有时候大家会情不自禁用一个被删除的对象来进行复制构造,抑或返回一个自动变量的指针等。 - new/delete不配对
有关new/delete的问题可以查看我的文章C++初始化方式 - 内存碎片
这个是一个复杂的问题。
右值
有关右值可以查看我的文章C++右值