1. C++11
1.1. nullptr常量
- 说明:nullptr是nullptr_t类型的字面值,代表空指针,它可以被转换成任一其他的指针类型
- 对比:
- 指针初始化为空指针:
int* p1 = 0; // C++98 int* p2 = NULL; // C++98 int* p3 = nullptr; // C++11
- 函数调用:
void F(int); void F(void*); void Func() { F(0); // 调用void F(int) F(NULL); // 可能调用void F(int),取决于NULL的宏定义值 F(nullptr); // 调用void F(void*) }
- 建议:优先选用nullptr, 而非0或NULL
1.2. constexpr关键字
- 说明:
- 算术类型,引用,指针类型属于字面值类型
- constexpr用于声明变量时表示该变量是一个编译期常量,变量必须是字面值类型
- constexpr用于声明函数时表示该函数可以在编译期得出运算结果
- C++11中constexpr函数必须遵守以下约定,在C++14中有所放宽:
- 函数的返回类型和形参类型都必须是字面值类型
- 函数体只能是一条return语句,其他语句不能在运行时执行任何操作,例如空语句,类型别名声明语句等
- 调用constexpr函数时,如果传入的参数都是编译期常量,则函数结果将在编译期计算出来;如果传入参数有一个到多个的值编译期未知,则运作方式与普通函数无异
- 对比:
- constexpr与const比较:constexpr对象一定是const对象,反之不成立
int Add1(int a, int b); // 普通函数 constexpr int Add2(int a, int b); // constexpr函数 void Func() { const int c1 = 10; // 编译期常量 const int c2 = c1 + 1; // 编译期常量 const int c3 = Add1(1, 2); // 运行时常量 int x1 = 10; // 非常量 constexpr int ce1 = 10; // 编译期常量 constexpr int ce2 = ce1 + 1; // 编译期常量 // constexpr int ce3 = Add1(1, 2); // 编译出错,Add1无法在编译期计算出结果 constexpr int ce4 = Add2(1, ce1); // 正确, 1和ce1都是编译期常量,Add2将在编译期计算出结果 // constexpr int ce5 = Add2(1, x1); // 错误, x1不是编译期常量,Add2运作方式与Add1相同 // 定义数组,数组维度必须是编译期常量 int arr1[10]; // 正确 int arr2[c1]; // 正确 // int arr3[c3]; // 错误 // int arr4[x1]; // 错误 int arr5[ce1]; // 正确 }
- 建议:
- 如果认为变量是一个编译期常量,就为变量加上constexpr声明
- 只要有可能,就为函数加上constexpr声明
1.3. using类型别名声明
- 说明:为某种类型定义另外一个名字, 格式:
using alias = type;
- 对比:
- using与typedef比较:
// 为char*定义别名 typedef char* cstring; using cstring = char*; // 定义函数指针类型别名 typedef void(*pFunc)(int, int); // pFunc是函数指针类型 using pFunc = void(*)(int, int); // pFunc是函数指针类型 using Func = void(int, int); // Func是函数类型, 不是指针类型,需要注意
- 建议:
- 优先使用using进行别名声明,using声明更加直观易懂
- 不要使用#define定义类型别名,宏仅仅是字符串,没有类型信息
1.4. auto关键字
- 说明:
- 使用auto定义变量时,编译器根据初始化表达式自动推导变量类型,因此变量必须初始化
- 使用auto可以定义持有lambda表达式的变量
- 对比:
- 变量定义
void Func() { // 初始化 int x1; // 未初始化,存在风险 // auto x2; // 编译错误 auto x3 = 4; // x3是int类型 auto x4 = x3; // x4是int类型,拷贝x3的值 auto& x5 = x3; // x5是int&类型,指向x3 const auto& x5 = x3; // x6是const int&类型,指向x3 // 避免类型转换 std::vector<int> vec = {1, 2, 3, 4}; int count1 = vec.size(); // 发生隐式类型转换, 编译告警 auto count2 = vec.size(); // 正确接收size()函数的返回值 // 简化变量定义 std::map<int, std::string> int2StrMap; int2StrMap.insert(std::make_pair(1, "hello")); std::map<int, std::string>::iterator iter1 = int2StrMap.begin(); // C++98 auto iter2 = int2StrMap.begin(); // C++11 // 接收lambda表达式 std::function<int(int)> func = [](int a) { return 5 + a; }; auto lambda1 = [](int a) { return 5 + a; }; }
- 建议:
- 优先使用auto声明变量, 而非显式型别。可以简化定义,保证变量被初始化,消除型别不匹配问题
- 当使用大括号表达式初始化auto声明的变量时, 变量类型会被推导为std::initializer_list
1.5. 范围for语句
- 说明:
- 范围for语句可以用来遍历容器的所有元素,格式为:
for (declaration : expression) { statement }
- 范围for语句用于包含能返回迭代器的begin()和end()成员的对象
- 对比:
- 遍历容器:
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // C++98 for (std::vector<int>::iterator iter1 = vec.begin(); iter1 != vec.end(); ++iter1) { std::cout << *iter; } for (int i = 0; i < vec.size(); ++i>) { std::cout << vec[i]; } // C++11 for (auto val : vec) { std::cout << val; }
- 建议:
- 当需要遍历整个容器而不需要元素的索引时,使用范围for语句而不是迭代器
- 当在范围for语句中使用auto时,内置类型的元素直接使用auto,需要修改元素时使用auto&,不需要修改元素时使用const auto&
- 一定不要在范围for语句中增删容器元素
1.6. lambda表达式与std::bind
- 说明:
- 一个lambda表达式表示一个可调用的代码块,可以理解为一个匿名函数,其格式为:
// 捕获列表中为lambda所在函数中定义的局部变量,如果没有mutable声明,则不能修改这些捕获的变量 // 形参列表,返回类型,函数体与其他普通函数一样 [捕获列表](形参列表) mutable -> 返回类型 { 函数体 }
- lambda表达式的结构中可以忽略形参列表、返回类型和mutable声明,但必须包含捕获列表和函数体:
void Func() { auto f = []{ return 5; } // 调用lambda表达式 std::cout << f() << std::endl; }
- 忽略返回类型时,如果函数体只是一个return语句,则返回类型根据表达式推导,否则返回void
- lambda表达式的形参列表中不能有默认参数
- lambda表达式的捕获列表只用于捕获函数中的局部非static变量,而局部static变量和函数之外声明的名字可以直接使用
- lambda捕获方式:
void Func() { int x1 = 5; const int x2 = 10; // 显式值捕获 auto lambda1 = [x1, x2](int a) { return x1 + x2 + a; }; // 显式值捕获x1,显式引用捕获x2 auto lambda2 = [x1, &x2](int a) { return x1 + x2 + a; }; // 默认值捕获x1,x2 auto lambda3 = [=](int a) { return x1 + x2 + a; }; // 默认引用捕获x1,x2 auto lambda4 = [&](int a) { return x1 + x2 + a; }; // 默认值捕获x1,显式引用捕获x2 auto lambda5 = [=, &x2](int a) { return x1 + x2 + a; }; // 默认引用捕获x1,显式值捕获x2 auto lambda6 = [&, x2](int a) { return x1 + x2 + a; }; }
- lambda本质上是一个函数对象(重载了operator()的类的对象):
int x = 10; auto lambda1 = [x](int a) { return x + a; }; // 等价于 class TempUniqueName { public: TempUniqueName(int x) : x_(x) {} int operator()(int a) { return x_ + a; } private: int x_; }; TempUniqueName lambda1(x);
- std::bind是std::bind1st和std::bind2nd的后继特性, 它接受一个可调用对象,返回一个新的可调用对象以新的形参列表, 其格式为:
auto newCallable = std::bind(callable, argList);
- 对比:
- lambda,函数指针,函数对象比较:
bool IntCompare(int a, int b) { return a > b; } class IntComparator { public: bool operator()(int a,int b) { return a > b; } } void Func() { std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 函数指针 std::sort(vec.begin(), vec.end(), IntCompare); // 函数对象 std::sort(vec.begin(), vec.end(), IntComparator()); // lambda std::sort(vec.begin(), vec.end(), [](int a, int b){ return a > b; }); }
- lambda, std::bind比较:
void F1(int a1, int a2, double b1, double b2); // 包装F1,使其只需传入a1和b1参数, a2和b2使用给定值 void Func() { // std::bind auto newF1 = std::bind(F1, std::placeholders::_1, 100, std::placeholders::_2, 10.0); newF1(50, 20.0); // 等价于F1(50, 100, 20.0, 10.0) // lambda auto lambdaF1 = [](int a1, double b1) { F1(a1, 100, b1, 10.0); } lambdaF1(50, 20.0); }
- 建议:
- 不要使用lambda的默认捕获方式,容易导致空悬指针问题
- 优先选用lambda,而不是std::bind:lambda可读性更好,表达力更强,可能运行效率也更高
1.7. =default/=delete
- 说明:
- 对类的特种成员函数(默认构造函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数),可以使用
=default
来显式的要求编译器生成合成的版本,这些合成的版本会逐成员调用其对应的特种函数, 例如合成的默认构造函数: 对于内置类型,执行默认初始化(随机值),对于类类型,调用其默认构造函数 - 使用
=default
声明的函数是隐式内联的,如果不希望是内联函数,则需要将=default
声明放在类外 - 对于任何函数(不仅是类的特种成员函数,虽然通常用于这些函数上)可以使用
=delete
要求编译器不定义这个函数,=delete
声明必须出现在函数第一次声明的时候 - 如果类的析构函数被声明
=delete
,那么编译器将不允许创建该类型的变量或临时对象,因为无法销毁该类的成员
- 对类的特种成员函数(默认构造函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数),可以使用
- 对比:
// = default class DefaultMember { public: DefaultMember() = default; // 合成默认构造函数, 隐式内联 DefaultMember(const DefaultMember&); // 合成拷贝构造函数, 非内联 DefaultMember& operator=(const DefaultMember&); // 合成拷贝赋值运算符,非内联 ~DefaultMember() = default; // 合成析构函数, 隐式内联 }; DefaultMember::DefaultMember(const DefaultMember&) = default; DefaultMember& DefaultMember::operator=(const DefaultMember&) = default; // = delete // C++98 class NonCopyable1 { // 声明为private成员函数,不定义这些函数,无法访问,因此阻止了拷贝 NonCopyable1(const NonCopyable1&); NonCopyable1& operator=(const NonCopyable1&); public: NonCopyable1(); ~NonCopyable1(); }; // C++11 class NonCopyable2 { public: NonCopyable2(); ~NonCopyable2(); // 删除的拷贝构造和拷贝赋值运算符 NonCopyable2(const NonCopyable2&) = delete; NonCopyable2& operator=(const NonCopyable2&) = delete; };
- 建议:
- 对于类的特种成员函数,如果希望使用编译器合成的版本,则使用
=default
显式声明这些函数 - 对于希望阻止拷贝的类应该使用
=delete
声明拷贝构造函数和拷贝赋值运算符,而不是将其声明为private成员
- 对于类的特种成员函数,如果希望使用编译器合成的版本,则使用
1.8. 右值引用与移动语义
- 说明:
- 左值和右值:
- 左值:变量(包括形参,右值引用),函数,返回左值引用的函数调用,内建赋值表达式,前置自增自减表达式,指针解引用表达式,内建下标表达式,字符串字面量,强转为左值引用类型的表达式
- 右值:字面量(字符串字面量除外),返回非引用的函数调用,后置自增自减表达式,内建的算术表达式、逻辑表达式、比较表达式,取地址表达式,强转为非引用类型的表达式,枚举项,lambda表达式
- 右值引用就是必须绑定到右值的引用,使用
&&
表示, 右值引用不能绑定到一个左值上:
int i = 42; int& r = i; // r是左值引用 // int&& rr = i; // 错误, 右值引用不能绑定到左值 // int& r2 = i * 42; // 错误, i * 42是一个右值 const int& r3 = i * 42; // 可以将const左值引用绑定到右值 int&& rr2 = i * 42; // rr2是右值引用 // int&& rr3 = rr2; // 错误, rr2也是左值
- 可以使用标准库函数
std::move
将一个左值转换成右值引用,当使用std::move
后这个左值只能被赋值或销毁,而不能直接使用它:
// 将左值i转换到右值,此后i可以被赋值,也可以直接销毁,在赋予i新值之前不能使用i的值 int&& rr4 = std::move(i);
- 移动构造函数/移动赋值运算符:类的这两个特种成员函数的形参是该类对象的右值引用,在函数体中要完成资源的移动,同时还要确保移后对象处于这样一个状态-销毁它是无害的,一旦移动完成,源对象必须不再指向被移动的资源。这两个函数只进行资源的移动,而不进行资源分配,不会抛出异常,因此需要加上
noexcept
标识符,否则移动操作不会触发,编译器会转而调用拷贝操作:
class Moveable { static constexpr int ARRAY_SIZE = 10; public: Moveable(int length = ARRAY_SIZE) : data_(new int[length]), len_(length) {} ~Moveable() { Free(); } // 拷贝控制 Moveable(const Moveable& other) : data_(new int[other.len_]), len_(other.len_) { for (int i = 0; i < len_; ++i) { data_[i] = other.data_[i]; } } Moveable& operator=(const Moveable& other) { // copy and swap,能正确处理自赋值 Moveable tmp(other); std::swap(data_, tmp.data_); std::swap(len_, tmp.len_); return *this; } // 移动控制 Moveable(Moveable&& other) noexcept : data_(other.data_), len_(other.len_) { other.data_ = nullptr; other.len_ = 0; } Moveable& operator=(Moveable&& other) noexcept { // copy and swap,能正确处理自赋值 Moveable tmp(std::move(other)); std::swap(data_, tmp.data_); std::swap(len_, tmp.len_); return *this; } // 对于拷贝赋值运算符和移动赋值运算符,有一种简洁的实现方法 // 形参为Moveable类型,对左值调用拷贝构造,对右值调用移动构造 Moveable& operator=(Moveable other) { std::swap(data_, other.data_); std::swap(len_, other.len_); return *this; } private: void Free() { if (data_) { delete[] data_; } } private: int* data_; int len_; };
- 拷贝左值,移动右值,如果没有移动控制函数,那么对右值也将进行拷贝操作
- 左值和右值:
- 对比:
- 建议:
- 对于使用
std::move
处理过的左值不要再使用其值 - 移动操作不是提高性能的灵药,应该假定移动操作不存在,成本高,未使用
- 对于使用
1.9. explicit/override/final/noexcept指示符
-
说明:
- 当类的构造函数只接受一个参数时,实际上定义了一种由该参数转换为该类类型的隐式转换规则,使用explicit可以抑制这种转换的发生:
class ImplicitConvertable { public: ImplicitConvertable(const std::string&); }; class ExplicitConvertable { public: explicit ExplicitConvertable(const std::string&); }; void F1(const ImplicitConvertable& obj); void F2(const ExplicitConvertable& obj); void Func() { std::string str = "hello"; F1(str); // 正确,从str隐式构造一个ImplicitConvertable对象传入F1 // F1("hello") // 错误,编译器只会自动进行一步类型转换,这里需要两步转换 F1(ImplicitConvertable(str)); //正确 // F2(str); // 错误,explicit抑制了隐式转换 F2(ExplicitConvertable(str)); // 正确,只能显式构造ExplicitConvertable对象 }
- 在继承体系中,可以使用
override
来显式说明意在重写基类中的虚函数:
class Base { public: virtual void F1(); // 虚函数第一次出现的地方添加virtual关键字 virtual void F2(int); }; class Derived : public Base { public: void F1() override; // 正确,重写基类虚函数void F1(); // void F2() override; // 错误,基类没有void F2()的虚函数 // void F3() override; // 错误,基类没有void F3()的虚函数 virtual void F4(); // 正确, 派生类定义自己的虚函数 };
- 使用
final
来阻止类被继承:
class NoDerived final {}; // NoDerived不能被继承 // class D : public NoDerived {}; // 错误, 不能从NoDerived继承
- 使用
noexcept
来制定一个函数不会抛出异常,这种函数可以简化调用处的代码,编译器也可以对该函数进行特殊优化.如果函数违反了异常说明而抛出了异常,那么程序将直接调用std::terminate结束程序:
void Func() noexcept; // 承诺Func不抛出异常
-
对比:
-
建议:
- 为类的单参数构造函数加上explicit指示符,可以防止隐式转换发生
- 在派生类中为想要改写的基类虚函数加上override指示符
- 为不想被继承的类加上final指示符
- 只要函数不抛出异常,就为其加上noexcept知识符
1.10. string数值转换函数
- 说明:
C++11中引入了一组用于数值和字符串之间相互转换的函数 - 对比:
- 与C标准库字符串转换函数比较:
// C库 int atoi (const char * str); // str转换到int long int atol ( const char * str ); // str转换到long double atof (const char* str); // str转换到double // C++ std::string to_string (val); // val转换到字符串, val可以是任何算数类型 int stoi (const string& str, size_t* idx = 0, int base = 10); // str 转换到int long stol (const string& str, size_t* idx = 0, int base = 10); // str 转换到long float stof (const string& str, size_t* idx = 0); // str 转换到float double stod (const string& str, size_t* idx = 0); // str 转换到double
- 建议: 使用C++风格的转换函数
1.11. std::array
- 说明:
- std::array与数组一样拥有固定大小,并且大小需要在编译期给定
- std::array不会像数组一样作为参数传递时退化为指针
- 对比:
void Func { // 内置数组 int arr1[10] = {0}; for (auto val : arr1) { std::cout << val; } for (int i = 0; i < 10; ++i>) { std::cout << arr1[i]; } // std::array std::array<int, 10> arr2; arr2.fill(0); std::cout << arr2.front(); std::cout << arr2.back(); for (auto val : arr2) { std::cout << val; } for (int i = 0; i < arr2.size(); ++i>) { std::cout << arr2.at(i); } }
- 建议:
- 如果需要固定大小的数组,那么应该使用std::array而不是内置数组, 因为std::array类型安全,提供了丰富的接口,性能与内置数组相同
1.12. 无序关联容器
- 说明:
- C++11定义了4个无序关联容器:unordered_map, unordered_set, unordered_multimap, unordered_multiset, 分别与有序关联容器:map, set, multimap, multiset对应
- 无序关联容器底层使用hash表实现, 因此不能假定容器中元素的排列顺序;有序关联容器底层使用红黑树实现,所有元素按照键值大小排列,遵循严格弱序准则
- 对于自定义类型,如果需要放入无序关联容器内时,需要给定该类型的hash计算方式和判等准则;需要放入有序关联容器时,只需要给定符合严格弱序的比较准则
- 无序关联容器的插入,查找,删除操作时间复杂度为O(1);有序关联容器的插入,查找,删除操作时间复杂度为O(lg(n))
- 对比:
// 自定义类型 class Data { public: int Val(); const std::string& Str(); private: int val_; std::string str_; }; // 比较准则,用于有序关联容器 struct DataCompare { bool operator()(const Data& l, const Data& r) const { return l.Val() < r.Val() ||((l.Val() == r.Val()) && l.Str() < r.Str()); } }; // hash函数,用于无序关联容器 struct DataHash { size_t operator()(const Data& data) const { return std::hash<int>()(data.Val()) ^ std::hash<std::string>()(data.Str()); } }; // 判等准则,用于无序关联容器 struct DataEqual { bool operator()(const Data& l, const Data& r) const { return (l.Val() == r.Val()) && (l.Str() == r.Str()); } }; void Func() { std::set<Data, DataCompare> orderedDataSet; std::unordered_set<Data, DataHash, DataEqual> unorderedDataSet; }
- 建议:
- 如果元素的顺序不重要,优先使用无序关联容器
1.13. 智能指针
- 说明:
- C++11提供了两种智能指针:std::shared_ptr和std::unique_ptr; 一种伴随类:std::weak_ptr, 只能配合std::shared_ptr使用
- std::shared_ptr提供了共享所有权语义,可以拷贝赋值多次,并且是线程安全的,当指向资源的最后一个shared_ptr销毁时,资源被释放
- std::unique_ptr提供了独占所有权语义,同一时刻,只能有一个unique_ptr对象指向资源,unique_ptr不可拷贝赋值,只能移动
- std::weak_ptr是一种弱引用, 指向shared_ptr管理的资源,但不拥有所有权,每次使用前需要检查资源是否有效
- std::shared_ptr的对象大小是固定的, std::unique_ptr的对象大小是不定的
- 对比:
// 自定义类型 struct Data { Data() : val(0), str() {} Data(int v, const std::string& s) : val(v), str(s) {} int val; std::string str; } void Func() { // std::uniqu_ptr std::uniqu_ptr<Data> up1; // 空指针 std::uniqu_ptr<Data> up2(new Data()); // 持有一个指向默认构造的Data对象的指针 std::uniqu_ptr<Data> up3 = std::make_unique<Data>();// C++14,持有一个指向默认构造的Data对象的指针 auto up4 = std::make_unique<Data>(1, "hello"); // C++14, 持有一个指向Data(1,"hello")对象的指针 // auto up5 = up2 // 错误, unique_ptr不可拷贝 auto up6 = std::move(up2); // 正确,unique_ptr可移动,资源所有权转移到up6, up2变成空指针 // std::shared_ptr与std::weak_ptr std::shared_ptr<Data> sp1; // 空指针 std::shared_ptr<Data> sp2(new Data()); // 持有一个指向默认构造的Data对象的指针 std::shared_ptr<Data> sp3 = std::make_shared<Data>();// 持有一个指向默认构造的Data对象的指针 auto sp4 = std::make_shared<Data>(1, "hello"); // 持有一个指向Data(1,"hello")对象的指针 auto sp5 = sp2; // 正确, shared_ptr可拷贝 // auto sp6 = up3; // 错误, 不能从unique_ptr拷贝资源 auto sp7 = std::move(up3); // 正确, 从unique_ptr移动资源 std::weak_ptr<Data> wp1; // 空指针 std::weak_ptr<Data> wp2(sp2); // 持有指向sp3所持资源的弱引用 if (!wp2.expired()) { // 判断与wp2关联的share_ptr是否已失效 auto sp8 = wp2.lock();//若expired()为true, lock()返回空的shared_ptr,否则返回有效的shared_ptr } std::shared_ptr<Data> sp9(wp2); // 从weak_ptr构造shared_ptr, 如果若expired()为true则抛出异常 }
- 建议:
- 不要使用std::auto_ptr
- 优先使用std::unique_ptr, 因为unique_ptr有与原始指针相同的性能
- 优先使用std::make_unique()(C++14)和std::make_shared(), 因为只进行一次内存分配,内存占用更小,性能更好
- 使用std::weak_ptr来替代可能空悬的std::shared_ptr
- 使用std::weak_ptr来解决std::shared_ptr可能导致的指针环路
1.14. std::function
- 说明:
- 可调用对象的概念: 函数,函数指针,函数对象,lambda表达式统称为可调用对象
- std::function内部可以保存一个可调用对象的副本,std::function对象本身也是可调用对象,调用std::function对象相当于调用其内部持有的可调用对象
- 对比:
// C++98, 一般只能通过函数指针传递可调用对象 typedef int(*pAddFunc)(int, int); int Add(int a, int b) { return a + b; } void Invoke1(pAddFunc fn) { int val = fn(2, 3); std::cout << val; } void Func1() { Invoke1(Add); } // C++11 using AddFunc = std::function<int(int, int)>; struct Adder { int operator()(int a, int b) { return a + b; } }; void Invoke2(AddFunc fn) { int val = fn(2, 3); std::cout << val; } void Func2() { Invoke2(Add); Invoke2(Adder()); Invoke2([](int a, int b) { return a + b; }); }
- 建议: 需要回调函数时,使用std::function接收可调用对象,而不是函数指针,可以简化调用逻辑
2. C++14
2.1. std::make_unique()
- 说明:
- std::make_unique()在C++14中加入标准库,原因是在C++11时标准委员会忘记了~~~
- 使用方法见1.13
- 对比:
- 建议: 优先使用std::make_unique(),而非显式分配资源
2.2. 泛型lambda/初始化捕获
- 说明:
- C++14中允许在lambda的形参列表中使用auto来接受任意类型的参数:
// 可以接受任意类型的参数,只要该类型支持+运算 void Func() { auto lambda1 = [](const auto& a, const auto& b) { return a + b; }; }
- C++14中允许在lambda的捕获列表中使用初始化捕获的方式来捕获局部变量:
void Func() { int num = 10; // 以初始化捕获的方式捕获局部变量num到val中 // 需要注意: = 两侧是两个作用域, 左侧为lambda的作用域,右侧为函数的作用域 auto lambda2 = [val = num](int a) { return val + a; }; }
- 对比:
- 泛型lambda:
void Func() { // C++11, 有多少种类型,就需要定义多少种lambda std::vector<int> intVec = {1, 2, 3, 4, 5}; auto lambda1 = [](int a) { std::cout << a; }; std::for_each(intVec.begin(), intVec.end(), lambda1); std::vector<double> doubleVec = {1.0, 2.0, 3.0, 4.0, 5.0}; auto lambda2 = [](double a) { std::cout << a; }; std::for_each(doubleVec.begin(), doubleVec.end(), lambda2); std::vector<std::string> strVec = {"hello, ", "zhong ", "dian ", "xing ", "fa"}; auto lambda3 = [](std::string a) { std::cout << a; }; std::for_each(strVec.begin(), strVec.end(), lambda3); // C++14, 只需要定义一个泛型lambda auto lambda4 = [](auto a) { std::cout << a; } std::for_each(intVec.begin(), intVec.end(), lambda4); std::for_each(doubleVec.begin(), doubleVec.end(), lambda4); std::for_each(strVec.begin(), strVec.end(), lambda4); }
- 初始化捕获:
void Func() { // 在lambda中捕获一个只可移对象 std::unique_ptr<int> up1 = std::make_unique<int>(5); // C++11, 十分麻烦 // C++14, 使用初始化捕获将up1移入lambda中 auto lambda1 = [up2 = std::move(up1)]() { std::cout << *up2; }; }
- 建议:
- 使用泛型lambda表达式可以简化代码,提高代码复用能力
- 捕获列表中使用初始化捕获方式将只可移对象移入lambda中
2.3. 更加宽松的constexpr函数
- 说明:
- C++14中放宽了constexpr函数的函数体不得包含多余一条可执行语句的规定,现在可以有任意多条可执行语句, 可以使用除了goto和try…catch以外的控制语句(if, while, for…)
- 对比:
// C++11 constexpr int pow(int base, int exp) noexcept { return (exp == 0 ? 1 : base * pow(base, exp - 1)); } // C++14 constexpr int pow(int base, int exp) noexcept { auto result = 1; for (int i = 0; i < exp; ++i) { result *= base; } return result; }
- 建议:
2.4. 二进制字面值和数字分隔符
- 说明:
- 使用前缀0b后接01数字串,即可创建一个二进制数字
- 可以在数字中使用’单引号作为分隔符,提升数字的可读性
- 对比:
// 二进制数字 // C++98及C++11:无 //C++14 int val = 0b1101000; // val的值为104 // 数字分隔符 int num1 = 1000000000; // C++98及C++11 int num2 = 1'000'000'000; // C++14 int num3 = 1'00000'00'00; // C++14,分隔符可以任意间隔
- 建议:
- 对较大的数字字面值,使用数字分隔符间隔三位隔开,以提升可读性
3. C++17
3.1. 结构化绑定
- 说明:
1.结构化绑定 绑定指定名称到初始化器的子对象或元素,其格式为:const/static(可选) auto(必选) &/&&(可选) [逗号分隔的标识符列表] = 表达式; const/static(可选) auto(必选) &/&&(可选) [逗号分隔的标识符列表]{表达式}; const/static(可选) auto(必选) &/&&(可选) [逗号分隔的标识符列表](表达式);
- 表达式可以是数组或非union类的任意类型
- 表达式是数组类型时:标识符列表中的每个标识符均成为指代数组的对应元素的左值。标识符的数量必须等于数组的元素数量:
int a[2] = {1,2}; auto [x,y] = a; // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1] auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]
- 表达式是类类型时:标识符列表中的每个标识符绑定到类的各个可访问非静态数据成员:
struct S { int x1; double y1; }; S f(); auto [x, y] = f(); // x是x1的拷贝, y是y1的拷贝
- 对比:
- std::map的循环遍历
void Func() { std::map<int, std::string> int2StrMap; ... // C++98 std::map<int, std::string>::iterator iter = int2StrMap.begin(); for (; iter != int2StrMap.end(); ++iter) { std::cout << iter->first << ", " << iter-second; } // C++11 for (const auto& pair : int2StrMap) { std::cout << pair.first << ", " << pair.second; } // C++17 for (const auto& [val, str] : int2StrMap) { std::cout << val << ", " << str; } }
- 建议:
3.1. 条件分支语句初始化
- 说明:
- C++17中,在if/switch条件语句中可以增加初始化语句:
if/switch (初始化语句; 条件) // 等价于 初始化语句; if/switch (条件) void Func() { std::set<int> intSet; ... if (auto it = intSet.find(10); it != intSet.end()) { std::cout << *it; } }
- 对比:
- 建议:
3.1. std::filesystem
- 说明:文件系统库
- 对比:
- 建议:
3.1. std::string_view
- 说明:对字符序列或字符串切片的只读非占有引用
- 对比:
- 建议:
3.1. std::optional
- 说明:用于表示可选对象
- 对比:
- 建议:
3.1. std::any
- 说明:用于保存任何类型的单个值
- 对比:
- 建议:
3.1. std::variant
- 说明:带有标记的联合容器
- 对比:
- 建议:
参考资料
- C++ Primer 5th : C++11大百科全书
- Effective Modern C++ : C++11/14的高效用法
- http://www.cplusplus.com/ : C++98/11参考手册
- https://zh.cppreference.com/w/cpp : C++最新标准参考手册