0%

现代C++

1. 语言特性

1.1 常量

nullptr代替NULL
传统C++会把NULL,0视为同一个东西,有些定义((void*)0),有些会定义为0,但是有问题:

  • C++不允许void * 隐式类型转换,void* 0
  • 0会给C++重载特性带来混乱

constexpr
明确声明函数或者对象在编译期会成为常量表达式, C++14开始,constexpr函数可以使用局部变量、循环、分支等简单语句。

1
2
3
4
5
6
constexpr int fibonacci(const int n) {
//c++11 编译不了
if(n == 1) return 1;
if(n == 2) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}

字面量:
C++14带来很多新的字面量:

1
2
3
4
5
6
7
8
9
10
11
using namespace std::literals::complex_literals;
std::cout << "i * i = " << 1i * 1i << std::endl;

using namespace std::literals::chrono_literals;
this_thread::sleep_for(500ms);

using namespace std::literals::string_literals;
std::cout << "hello world"s.substr(0, 5);

#include <bitset>
cout << bitset<9>(mask) << endl;

1.2 变量及其初始化

C++17 可以将变量放在语句内(C++17)

1
2
3
4
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}

初始化列表(C++11)

1
2
3
4
5
6
7
// std::initializer_list
std::vector<int> vec;
MagicFoo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin(); it != list.end(); it++) {
vec.push_back(*it);
}
}

几乎可以在所有初始化对象的地方使用大括号而不是小括号。当一个构造函数没有标成 explicit 时,你可以使用大括号不写类名来进行构造:

1
2
3
4
Obj getObj()
{
return {1.0};
}

和Obj(1.0)唯一区别在于{1.0}拒绝窄转换,只允许调用Obj(double).

结构化绑定(C++17)

1
2
//f() return std::<int, double, std::string>
auto [x, y, z] = f();

类数据成员的默认初始化(C11):

1
2
3
4
5
6
7
8
9
10
11
12

class Complex {
public:
Complex() {}
Complex(float re) : re_(re) {}
Complex(float re, float im)
: re_(re) , im_(im) {}

private:
float re_{0};
float im_{0};
};

内联变量(C++17)
C++17 引入了内联(inline)变量的概念,允许在头文件中定义内联变量,然后像内联函数一样,只要所有的定义都相同,那变量的定义出现多次也没有关系。对于类的静态数据成员,const 缺省是不内联的,而 constexpr 缺省就是内联的。

1
struct magic { static inline const int number = 42;};

1.3 类型推导

auto
- 自动类型推断
- C++ 20开始可以函数传参
- C++14开始可以用于返回值(包括decltype)

auto实际使用规则类似于函数模板参数推导
- auto a = expr 意味着用expr去匹配一个假想的 template <typename T> f(T) 函数模板,结果为值类型
- const auto& a = expr 意味着expr去匹配一个 template <typename T> f(const T&) 结果为常左值引用类型
- auto&& a = expr; 意味着 template <typename T> f(T&&) 函数模板

即根据类型推导规则,auto 是值类型,auto& 是左值引用类型,auto&& 是转发引用(可以是左值引用,也可以是右值引用)。

decltype

  • decltype(变量名) 可以获得变量名的精确类型
  • decltype(表达式) -> 获得表达式的引用
    • 如果是个纯右值(prvalue),结果仍然是值类型

尾返回类型(C++11)

1
2
3
4
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x + y) {
return x + y;
}

返回值推导(C++14):

1
2
3
4
template<typename T, typename U>
auto add3(T x, U y) {
return x + y;
}

decltype(auto) (C++14)
写auto时要确定引用还是值类型,decltype(auto)可以根据表达式通用地决定返回的是值类型还是引用类型。

1
2
3
decltype(auto) look_up_string() {
return lookup1();
}

类模板实参推导 (CTAD)(C++17 起)

1
2
std::pair pr{1, 42};
std::array a{1,2,3};

1.4 控制流

if constexpr (C++17)
允许代码中声明常量表达式的判断:

1
2
3
4
5
6
7
8
9
#include <iostream>
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}

区间for(C++11)

1
2
3
for (auto element : vec) {

}

1.5 模板

模板的哲学在于将问题丢到编译期去处理,大幅度优化运行时的性能,C++的黑魔法之一。

外部模板
为了解决重复实例化的问题,C++11 引入外部模板:

1
2
template class std::vector<bool>; // 强制实例化
extern template class std::vector<double>; // 不在当前文件中实例化

using 语法用作类型别名(C++11)

1
2
3
4
5
6
7
8
9
10
11
12
typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;

// 不合法
// template<typename T>
// typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;

int main() {
TrueDarkMagic<bool> you;
}

变长参数模板(C++11)
参数是可变的,但是如何解包(把类型拿下来)有以下处理:

  • 递归模板函数(C++11),缺点在需要定义一个终止递归的函数
  • 变参模板展开(C++17)
  • 初始化列表展开
1
2
3
4
5
6
7
8
9
10
11
//递归模板函数
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}

template <typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}

变参模板展开(C++17),在一个函数中完成printf的编写

1
2
3
4
5
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...);
}

初始化列表展开

1
2
3
4
5
6
7
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}

1.6 面向对象

委托构造(C++11)
构造函数可以调用另一个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() {
value2 = value;
}
};

int main()
{
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}

继承构造(C++11)
使用using 引入继承构造的概念

1
2
3
4
5
#include <iostream>
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};

显式虚函数重载(C++11

  • override 通知编译器进行重载
  • final
    • 函数 final 表示
    • 类 final 表示拒绝重载
  • default
  • delete

枚举类

1
2
3
enum class new_enum : unsigned int {
value1, value2, value3 = 100, value4 = 100
};

2. 运行时强化

2.1 Lambda 表达式

捕获方式:

  • 值捕获 [value]
  • 引用捕获 [&value]
  • 隐式捕获
    • [&]
    • [=]
  • 表达式捕获 C++ 14
    • 右值传递
  • Lambda 泛型
    • auto 用在参数表中

2.2 函数包装器

  • 各种可调用对象
  • 方便的函数容器
1
2
3
std::function<int(int)> func2 = [&](int value)->int {
return 1 + value;
};

2.3 移动和右值

2.3.1 值类别

值类别和值类型的差别:

  • 值类别 value category:左右值
  • 值类型/引用类型 value type:C++中,只有引用和指针是引用类型,Java中原生类型是值类型,类属于引用类型,Python中都是引用类型
  • lvalue: 有标识符,可取地址的表达式,函数变量名,返回左值引用的表达式,字符串字面量
  • rvalue :包括prvalue 纯右值和xvalue将亡值
  • prvalue: 纯右值,传统右值:没有标识符,不可以取地址的表达式,例如++x,除了字符串字面量之外的字面量,prvalue如果绑定到一个引用上,可以延长生命周期
  • xvalue:将亡值,可以看成有名字的右值(有标识符)
  • glvalue :现在的左值范围,lvalue和xvalue

2.3.2 移动

std::move : 无条件地将实参强制类型转换为右值,产生一个xvalue

原理: static_cast 到type &&

1
2
3
4
5
6
7
8
9
10
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept{
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}

移动的意义:
string res = string("hello") + name + ".";在C++之前是完全不推荐的,但是有了移动之后过程:

  1. 调用 string(const *),生成Hello临时对象,复制一次
  2. 调用 operator+(string&&, const string&) 直接在1临时对象上操作,name复制一次
  3. 调用 operator+(string&&, const string&) 在后面追加操作
  4. 临时对象2析构
  5. 临时对象1析构

对于实际内存布局而言, 例如A类中有B,C类,在Java或者Python这样的语言中存储的实际是指针(类似)。保证了内存访问的局部性,这在现代处理器架构中是有性能优势的。缺点是复制对象的开销大大增加,故有移动语义这样的东西存在。

2.3.3 完美转发

std::forward

万能引用:

1
2
3
4
temmplate <typename T>
void f(T&& arg) {}
// T&&称为万能引用
// auto&&也是万能引用

如果是左值引用,则重载到&&,引用折叠,还是左值。如果是右值引用,重载到&&,引用折叠,是右值。这里的重载是为了检查右值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept {
return static_cast<_Tp&&>(__t);
}

/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}

引用坍缩和完美转发

引用坍缩主要是由于模板的推导结果可能是引用。即,对于

1
template <typename T> foo(T&&);

如果传递左值,则T推导为左值引用,如果传递是右值,则T推导为类型本身。T&&保持值类别进转发,即做到了完美转发。

3. 智能指针

实现一个智能指针:

  • unique_ptr
    • 需要考虑赋值运算符函数的问题
    • 实现*, -> 等运算符函数即可
    • 在C++11之前auto_ptr实现有问题
      • 当一不小心传递给另外一个ptr,你就不再拥有这个对象了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// auto_ptr 
//smart_ptr& operator=(smart_ptr& rhs) {
// smart_ptr(rhs).swap(*this);
// return *this;
//}

smart_ptr& operator=(smart_ptr rhs) {
rhs.swap(*this);
return *this;
}
void swap(smart_ptr& rhs) {
using std::swap;
swap(m_ptr, rhs.m_ptr);
}

shared_ptr实现(不考虑多线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class my_shared_ptr {
public:
template <typename U>
friend class my_shared_ptr;
// default constructor
explicit my_shared_ptr(T* ptr = nullptr) : m_ptr(ptr) {
if (ptr) {
m_shared_count = new shared_count();
}
}

// copy constructor func
my_shared_ptr(const my_shared_ptr& other) {
m_ptr = other.m_ptr;
if (m_ptr) {
other.m_shared_count->add_count();
m_shared_count = other.m_shared_count;
}
}

// copy constructor func for pointer cast
template <typename U>
my_shared_ptr(const my_shared_ptr<U>& other) noexcept {
m_ptr = other.m_ptr;

if (m_ptr) {
other.m_shared_count->add_count();
m_shared_count = other.m_shared_count;
}
}

// dynamic cast copy constructor
template <typename U>
my_shared_ptr(const my_shared_ptr<U>& other, T* ptr) {
m_ptr = ptr;
if (m_ptr) { // if m_ptr is empty, do not add the count
other.m_shared_count->add_count();
m_shared_count = other.m_shared_count;
}
}

// move constructor func
template <typename U>
my_shared_ptr(my_shared_ptr<U>&& other) noexcept {
m_ptr = other.m_ptr;
if (m_ptr) {
m_shared_count = other.m_shared_count;
// move from other
other.m_ptr = nullptr;
}
}

// operator =
my_shared_ptr& operator=(my_shared_ptr rhs) noexcept {
rhs.swap(*this);
return *this;
}

// deconstructor func
~my_shared_ptr() {
if (m_ptr && !m_shared_count->reduce_count()) {
delete m_ptr;
delete m_shared_count;
}
}

public:
long use_count() const {
if (m_ptr) {
return m_shared_count->get_count();
} else {
return 0;
}
}

T* get() const noexcept {
return m_ptr;
}

T& operator*() const noexcept {
return *m_ptr;
}

T* operator->() const noexcept {
return m_ptr;
}

operator bool() const noexcept {
return m_ptr;
}
private:
// swap function to swap two shared_pointers
void swap(my_shared_ptr& rhs) {
using std::swap;
swap(m_ptr, rhs.m_ptr);
swap(m_shared_count, rhs.m_shared_count);
}

private:
T* m_ptr;
shared_count* m_shared_count;
}

4. STL

Array
优点:

  • C 数组作为参数有退化行为,传递给另外一个函数后那个函数不再能获得 C 数组的长度和结束位置
  • C 数组没有良好的复制行为,无法作为键类型