C++11特性整理(2022/04/09 补充)
本文最后更新于 1120 天前,其中的信息可能已经有所发展或是发生改变。

只关注C++11,C++14、17、20乃至未来的23另开篇章;

主要来源:https://zh.cppreference.com/w/cpp/11

次要来源会在提及处给出;

1.类内初始化器

#include<iostream>
using namespace std;

class node
{
public:
	bool ifPosi{true};//true=posi false=nega
	long long integerPart{0};
	long long numerator{0};
	long long denominator{1};
	//node() :ifPosi{ true }, integerPart{ 0 }, numerator{ 0 }, denominator{ 1 }{};
};

int main()
{
	node a;
	cout << a.denominator;
	return 0;

无需定义构造函数,无需初始化所有变量,构造函数只需要关注与缺省值不同的变量即可;

大括号也可以用等号的形式表达;

2.auto

auto x = expr; //从初始化器推导类型

例如,给定const auto& i = expr; 则 i 的类型恰是某个虚构模板 template<class U> void f(const U& u) 中参数 u 的类型(假如函数调用 f(expr) 通过编译)。因此,取决于初始化器,auto&& 可被推导成左值引用或右值引用类型,这被用于基于范围的 for 循环。

如果用占位类型说明符声明多个变量,那么推导出的类型必须互相匹配。例如,声明 auto i = 0, d = 0.0; 非良构,而声明 auto i = 0, *p = &i; 良构并将 auto 推导为 int。

new 表达式中的类型标识。从初始化器推导类型。对于 new T init(其中 T 含占位符类型,而 init 是带括号的初始化器或带花括号的初始化器列表),如同在虚设的声明 T x init; 中对变量 x 一般推导 T 的类型。

auto 说明符也可以用于后随尾随返回类型的函数声明符,此时返回类型为其尾随返回类型(它也可以是占位符类型)。
auto (p)() -> int; // 声明指向【返回 int 的函数】的指针

auto (q)() -> auto = p; // 声明 q 为指向【返回 T 的函数】的指针 ,其中 T 从 p 的类型推导

不能使用 auto 推导的4种情况(编译器报错)[来源]

  1)auto 不能用于函数形参类型。

  2)对于结构体来说,非静态成员变量的类型不能是 auto 的。

  3)auto 能不声明数组类型。

  4)在实例化模版的时候不能使用 auto 作为模版参数。

#include <iostream>
#include <utility>
 
template<class T, class U>
auto add(T t, U u) { return t + u; } // 返回类型是 operator+(T, U) 的类型
 
// 在其所调用的函数返回引用的情况下
// 函数调用的完美转发必须用 decltype(auto)
template<class F, class... Args>
decltype(auto) PerfectForward(F fun, Args&&... args) 
{ 
    return fun(std::forward<Args>(args)...); 
}
 
template<auto n> // C++17 auto 形参声明
auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能从花括号初始化器列表推导
{
    return {n, n};
}
 
int main()
{
    auto a = 1 + 2;          // a 的类型是 int
    auto b = add(1, 1.2);    // b 的类型是 double
    static_assert(std::is_same_v<decltype(a), int>);
    static_assert(std::is_same_v<decltype(b), double>);
 
    auto c0 = a;             // c0 的类型是 int,保有 a 的副本
    decltype(auto) c1 = a;   // c1 的类型是 int,保有 a 的副本
    decltype(auto) c2 = (a); // c2 的类型是 int&,它是 a 的别名
    std::cout << "通过 c2 修改前,a = " << a << '\n';
    ++c2;
    std::cout << "通过 c2 修改后,a = " << a << '\n';
 
    auto [v, w] = f<0>(); // 结构化绑定声明
 
    auto d = {1, 2}; // OK:d 的类型是 std::initializer_list<int>
    auto n = {5};    // OK:n 的类型是 std::initializer_list<int>
//  auto e{1, 2};    // C++17 起错误,之前是 std::initializer_list<int>
    auto m{5};       // OK:DR N3922 起 m 的类型是 int,之前是 initializer_list<int>
//  decltype(auto) z = { 1, 2 } // 错误:{1, 2} 不是表达式
 
    // auto 常用于无名类型,例如 lambda 表达式的类型
    auto lambda = [](int x) { return x + 3; };
 
//  auto int x; // 于 C++98 合法,C++11 起错误
//  auto x;     // 于 C 合法,于 C++ 错误
 
    [](...){}(c0, c1, v, w, d, n, m, lambda); // 阻止“变量未使用”警告
}

3.decltype

检查实体的声明类型,或表达式的类型和值类别;

简单的说,就是获得括号内变量的类型然后作为声明;

例如:

const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int &

struct A { double x; };
const A* a;
 
decltype(a->x) y;       // y 的类型是 double(其声明类型)
decltype((a->x)) z = y; // z 的类型是 const double&(左值表达式)
 
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) // 返回类型依赖于模板形参
{                                     // C++14 开始可以推导返回类型
    return t+u;
}

auto f = [](int a, int b) -> int
{
    return a * b;
};

decltype(f) g = f; // lambda 的类型是独有且无名的

小结:[来源]

①作用于变量直接得到变量的类型;

②作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;

③作用于函数名会得到函数类型,不会自动转换成指针。

4.预置函数default

只需在函数声明后加上”=default;”,就可将该函数声明为defaulted函数,编译器将为显式声明的defaulted函数自动生成函数体。只能用于特殊成员函数【和比较运算符函数 (C++20 起),可以让编译器自己生成六种比较运算】

[代码来源]

#include "default.hpp"
#include <iostream>
 
 
// reference: http://www.learncpp.com/cpp-tutorial/b-6-new-virtual-function-controls-override-final-default-and-delete/
class Foo
{
	Foo(int x); // Custom constructor
	Foo() = default; // The compiler will now provide a default constructor for class Foo as well
};
 
/
// reference: http://en.cppreference.com/w/cpp/language/default_constructor
struct A
{
	int x;
	A(int x = 1) : x(x) {} // user-defined default constructor
};
 
struct B : A
{
	// B::B() is implicitly-defined, calls A::A()
};
 
struct C
{
	A a;
	// C::C() is implicitly-defined, calls A::A()
};
 
struct D : A
{
	D(int y) : A(y) {}
	// D::D() is not declared because another constructor exists
};
 
struct E : A
{
	E(int y) : A(y) {}
	E() = default; // explicitly defaulted, calls A::A()
};
 
struct F
{
	int& ref; // reference member
	const int c; // const member
	// F::F() is implicitly defined as deleted
};
 
int test_default1()
{
	A a;
	B b;
	C c;
	// D d; // compile error
	E e;
	// F f; // compile error
 
	return 0;
}
 
///
// reference: https://msdn.microsoft.com/zh-cn/library/dn457344.aspx
struct widget
{
	widget() = default;
 
	inline widget& operator=(const widget&);
};
 
// Notice that you can default a special member function outside the body of a class as long as it’s inlinable.
inline widget& widget::operator=(const widget&) = default;

简单的说,原本C++在已有构造函数的情况下不会有默认构造函数,导致增加了工作量,所以就有了default,编译器将为显式声明的 default 函数自动生成函数体;

同理,也可以将虚析构函数声明为default,一般而言编译器生成的代码效率要高于手动定义;

5.弃置函数delete

如果使用特殊语法 = delete ;取代函数体,那么该函数被定义为弃置的(deleted)。任何弃置函数的使用都是非良构的(程序无法编译)。这包含调用,包括显式(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用),构成指向弃置函数的指针或成员指针,甚至是在不求值表达式中使用弃置函数。但是可以隐式 ODR 使用刚好被弃置的非纯虚成员函数。

函数的弃置定义必须是翻译单元中的首条声明;已经声明过的函数不能声明为弃置的;

简而言之,就是对于不再使用的函数可以声明为delete;

6.final说明符

指定某个虚函数不能在派生类中被覆盖,或者某个类不能被派生;

struct Base
{
    virtual void foo();
};
 
struct A : Base
{
    void foo() final; // Base::foo 被覆盖而 A::foo 是最终覆盖函数
    void bar() final; // 错误: bar 非虚,因此它不能是 final 的
};
 
struct B final : A // struct B 为 final
{
    void foo() override; // 错误:foo 不能被覆盖,因为它在 A 中是 final 的
};
 
struct C : B // 错误:B 是 final 的
{
};

7.override说明符

在成员函数的声明或定义中,override 说明符确保该函数为虚函数并覆盖某个基类中的虚函数;

override 是在成员函数声明符之后使用时拥有特殊含义的标识符:其他情况下它不是保留的关键词;

struct A
{
    virtual void foo();
    void bar();
};
 
struct B : A
{
    void foo() const override; // 错误:B::foo 不覆盖 A::foo
                               // (签名不匹配)
    void foo() override; // OK:B::foo 覆盖 A::foo
    void bar() override; // 错误:A::bar 非虚
};

8.尾随返回类型

非指针声明符 ( 形参列表 ) cv限定符(可选) 引用限定符(可选) 异常说明(可选) 属性(可选) -> 尾随返回类型;

尾随返回类型只能在最外层函数声明符中使用。此时声明说明符序列必须包含关键词 auto;

返回类型取决于实参名时或当返回类型复杂时使用尾随返回类型;

如果函数声明的声明说明符序列包含关键词 auto,那么尾随返回类型可以省略,且编译器将从返回语句中所用的表达式的类型推导出它。如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行;

template<typename T1, typename T2>
auto sum(const T1 &t1, const T2 &t2)->decltype(t1+t2) 
{
    return t1 + t2;
}

template<typename T1, typename T2>
auto mul(const T1 &t1, const T2 &t2)->decltype(t1*t2) 
{
    return t1 * t2;
}

举例而言,对于int (*func(int i))[10];无法直观的获得它的返回类型,于是就可以使用尾随返回类型:

auto func(int i) -> int(*)[10];

(网上关于这个的资料很少,不知道还有没有其它的用处或说是好处)

9.右值引用

右值引用声明符:声明 S&& D; 是将D声明为声明说明符序列S所确定的类型的右值引用;

引用折叠:通过模板或 typedef 中的类型操作可以构成引用的引用,此时适用引用折叠(reference collapsing)规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用:

typedef int&  lref;
typedef int&& rref;
int n;
lref&  r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref&  r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&

右值引用可用于为临时对象延长生存期

#include <iostream>
#include <string>
 
int main()
{
    std::string s1 = "Test";
//  std::string&& r1 = s1;           // 错误:不能绑定到左值
 
    const std::string& r2 = s1 + s1; // okay:到 const 的左值引用延长生存期
//  r2 += "Test";                    // 错误:不能通过到 const 的引用修改
 
    std::string&& r3 = s1 + s1;      // okay:右值引用延长生存期
    r3 += "Test";                    // okay:能通过到非 const 的引用修改
    std::cout << r3 << '\n';
}

引用转发:一种特殊的引用,它保持函数实参的值类别,使得 std::forward 能用来转发实参;

1) 函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无 cv 限定的右值引用:

template<class T>
int f(T&& x) {                    // x 是转发引用
    return g(std::forward<T>(x)); // 从而能被转发
}
int main() {
    int i;
    f(i); // 实参是左值,调用 f<int&>(int&), std::forward<int&>(x) 是左值
    f(0); // 实参是右值,调用 f<int>(int&&), std::forward<int>(x) 是右值
}
 
template<class T>
int g(const T&& x); // x 不是转发引用:const T 不是无 cv 限定的
 
template<class T> struct A {
    template<class U>
    A(T&& x, U&& y, int* p); // x 不是转发引用:T 不是构造函数的类型模板形参
                             // 但 y 是转发引用
};

2) auto&&,但当其从花括号包围的初始化器列表推导时除外:

auto&& vec = foo();       // foo() 可以是左值或右值,vec 是转发引用
auto i = std::begin(vec); // 也可以
(*i)++;                   // 也可以
g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别
 
for (auto&& x: f()) {
  // x 是转发引用;这是使用范围 for 循环最安全的方式
}
 
auto&& z = {1, 2, 3}; // *不是*转发引用(初始化器列表的特殊情形)

来源

在C++11之前,只能对左值进行引用如:

int num = 10;
const int &b = num;

所以当我们需要对右值进行修改时(如移动语义),便无法进行,于是右值引用诞生了;

和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化;

C++ 语法上是支持定义常量右值引用但毫无用处,右值引用主要用于移动语义和完美转发;

10.移动构造函数

在以前,如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作,于是我们开始使用移动而非深拷贝的方式初始化含有指针成员的类对象;

所谓移动语义,就是把原本程序执行过程中产生的只用于传递数据的临时对象,将其指针浅拷贝,然后修改指针指向,从而提高初始化速度;

[代码来源]

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }

    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

在上述代码中,通过新增右值引用参数的构造函数(即移动构造函数)的形式,避免了同一块空间被释放多次的情况发生;

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数;

如果不对类(struct、class、union)提供任何用户定义的移动构造函数,且没有用户声明的复制构造函数、复制赋值运算符、移动赋值运算符以及析构函数,则编译器将声明一个移动构造函数作为这个类的非explicit的inline public成员,签名为T::T(T&&);

一个类可以拥有多个移动构造函数,当存在用户定义的移动构造函数时,用户仍然可以通过关键词 default 强制编译器生成隐式声明的移动构造函数;

11.移动赋值运算符

类T的移动赋值运算符是名为operator=的非模板非静态成员函数,同时10中提到的隐式声明的移动赋值运算符同样适用于移动赋值运算符;

当然现在的编译器都会做相关的优化;

12.有作用域枚举

enum struct|class 名字 { 枚举项 = 常量表达式 , 枚举项 = 常量表达式 , … }(1)
enum struct|class 名字 : 类型 { 枚举项 = 常量表达式 , 枚举项 = 常量表达式 , … }(2)
enum struct|class 名字 ;(3)
enum struct|class 名字 : 类型 ;(4)

每个枚举项都成为该枚举的类型的具名常量,它被该枚举的作用域所包含,且可用作用域解析运算符访问。没有从有作用域枚举项到整数类型的隐式转换,尽管 static_cast 可以用来获得枚举项的数值;

enum class Color { red, green = 20, blue };
Color r = Color::blue;
 
switch(r)
{
    case Color::red  : std::cout << "红\n"; break;
    case Color::green: std::cout << "绿\n"; break;
    case Color::blue : std::cout << "蓝\n"; break;
}
 
// int n = r; // 错误:不存在从有作用域枚举到 int 的隐式转换
int n = static_cast<int>(r); // OK, n = 21

简单的说,就是不允许隐式转换为整型(但依旧可以显式强制转换),从而提高类型安全;

13.constexpr说明符

constexpr说明符声明编译时可以对函数或变量求值值。这些变量和函数可用于需要编译期常量表达式的地方;

与const类似,与之不同的是,constexpr是某种意义上的常量,编译器会在编译时进行充分的优化;

[来源]

const int func() {
    return 10;
}
main(){
  int arr[func()];
}
//error : 函数调用在常量表达式中必须具有常量值

-------------
constexpr func() {
    return 10;
}
main(){
  int arr[func()];
}
//编译通过

同时,如果上述代码中返回的是变量时,编译依旧能够通过,并且编译器会将之作为变量处理而非常量;

总结地说,const是全过程的常量(准确的说是一个不能修改的、只读的变量),而constexpr只是编译期常量;

所以说对于需要常量的语境使用constexpr,需要只读的语境使用const;

14.字面类型(LiteralType)

指明一个类型为字面类型。字面类型是 constexpr 变量所拥有的类型,且能通过 constexpr 函数构造、操作及返回它们;

(不是很理解这段话,网上相关资料基本没有)

15.列表初始化

#include <iostream>
#include <vector>
#include <map>
#include <string>
 
struct Foo {
    std::vector<int> mem = {1,2,3}; // 非静态成员的列表初始化
    std::vector<int> mem2;
    Foo() : mem2{-1, -2, -3} {} // 构造函数中的成员列表初始化
};
 
std::pair<std::string, std::string> f(std::pair<std::string, std::string> p)
{
    return {p.second, p.first}; // return 语句中的列表初始化
}
 
int main()
{
    int n0{};     // 值初始化(为零)
    int n1{1};    // 直接列表初始化
    std::string s1{'a', 'b', 'c', 'd'}; // initializer_list 构造函数调用
    std::string s2{s1, 2, 2};           // 常规构造函数调用
    std::string s3{0x61, 'a'}; // initializer_list 构造函数偏好 (int, char)
 
    int n2 = {1}; // 复制列表初始化
    double d = double{1.2}; // 纯右值的列表初始化,然后复制初始化
    auto s4 = std::string{"HelloWorld"}; // 同上, C++17 起不创建临时对象
 
    std::map<int, std::string> m = { // 嵌套列表初始化
           {1, "a"},
           {2, {'a', 'b', 'c'} },
           {3, s1}
    };
 
    std::cout << f({"hello", "world"}).first // 函数调用中的列表初始化
              << '\n';
 
    const int (&ar)[2] = {1,2}; // 绑定左值引用到临时数组
    int&& r1 = {1}; // 绑定右值引用到临时 int
//  int& r2 = {2}; // 错误:不能绑定右值到非 const 左值引用
 
//  int bad{1.0}; // 错误:窄化转换
    unsigned char uc1{10}; // 可以
//  unsigned char uc2{-1}; // 错误:窄化转换
 
    Foo f;
 
    std::cout << n0 << ' ' << n1 << ' ' << n2 << '\n'
              << s1 << ' ' << s2 << ' ' << s3 << '\n';
    for(auto p: m)
        std::cout << p.first << ' ' << p.second << '\n';
    for(auto n: f.mem)
        std::cout << n << ' ';
    for(auto n: f.mem2)
        std::cout << n << ' ';
}

规范了初始化,等号可有可无,原本用new圆括号的地方也可以使用初始化列表,如int* a = new int{123};

堆上动态分配的数组也可以使用初始化列表进行初始化,如int* arr = new int[3] { 1, 2, 3 };

此外,还可以使用在函数返回值上,如

struct Foo
{
    Foo(int, double) {}
};
Foo func(void)
{
    return { 123, 321.0 };
}

同时,列表初始化禁止窄化转换(隐式);

16.委托构造函数

如果类自身的名字在初始化器列表中作为类或标识符出现,那么该列表只能由这一个成员初始化器组成;这种构造函数被称为委托构造函数

class Foo
{
public: 
    Foo(char x, int y) {}
    Foo(int y) : Foo('a', y) {} // Foo(int) 委托到 Foo(char, int)
};

简单的说就是某个构造函数使用了同类的其它构造函数进行自己的初始化;

17.继承构造函数

[来源]

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
  //...等等系列的构造函数版本号
};
struct B:A
{
  B(int i):A(i){}
  B(double d,int i):A(d,i){}
  B(folat f,int i,const char* c):A(f,i,e){}
  //......等等好多个和基类构造函数相应的构造函数
};

当遇到上述基类拥有为数众多的不同版本号的构造函数,则在派生类中需要写非常多相对应的构造函数;

于是,我们使用using解决这个问题,如下所示:

struct Base
{
  void f(double i){
  cout<<"Base:"<<i<<endl;
  }
};
 
struct Drived:Base
{
  using Base::f;
  void f(int i){
    cout<<"Drived:"<<i<<endl;
  }
};

从而使派生类继承基类中全部的构造函数,需要注意的是,参数的默认值不会被继承,同时当继承多个类构造函数时,需要注意是否会发生冲突,对于冲突的函数可以额外写一个以覆盖;

同时,使用继承构造函数后编译器不会生成默认构造函数;

18.nullptr

关键词 nullptr 代表指针字面量。它是 std::nullptr_t 类型的纯右值。存在从 nullptr 到任何指针类型及任何成员指针类型的隐式转换。同样的转换对于任何空指针常量也存在,空指针常量包括 std::nullptr_t 的值,以及宏 NULL;

这个应该很熟悉了,一直在用,目前用来取代NULL来表示空指针;

19.long long

目标类型将有至少 64 位的宽度;

long long等价于long long int;

20.char16_t 与 char32_t

char16_t – UTF-16 字符表示的类型,要求大到足以表示任何 UTF-16 编码单元(16 位)。它与 std::uint_least16_t 具有相同的大小、符号性和对齐,但它是独立的类型;
char32_t同理(将上述的话16replace为32);

先来了解下wchar_t,这家伙以前课设时偶尔用到过;

因为char中有很多没有的字符,所以就有了wchar_twchar_t一般为16或32位,使用wcin和wcout进行输入输出,另外可以通过加上前缀L来指示宽字符常量和宽字符串;

同样的理由,wchar_t也不够用了,于是诞生了char16_t 与 char32_t,前缀u表示前者,前缀U表示后者;

21.类型别名&别名模板

using 标识符 属性(可选) = 类型标识 ;(1)
template < 模板形参列表 >
using 标识符 属性(可选) = 类型标识 ;
(2)

作用范围更广的typedef,当仅作用于类型别名时和typedef一致;

如下所示:

template<class T>
struct Alloc { };
template<class T>
using Vec = vector<T, Alloc<T>>; // 类型标识为 vector<T, Alloc<T>>
Vec<int> v; // Vec<int> 等同于 vector<int, Alloc<int>>
---------------------------
template<typename...>
using void_t = void;
template<typename T>
void_t<typename T::foo> f();
f<int>(); // 错误,int 没有嵌套类型 foo
---------------------------
template<class T>
struct A;
template<class T>
using B = typename A<T>::U; // 类型标识为 A<T>::U
template<class T>
struct A { typedef B<T> U; };
B<short> b; // 错误:B<short> 通过 A<short>::U 使用其自身类型

22.变参数模板

模板形参包是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。至少有一个形参包的模板被称作变参模板;

声明可变参数模板时需要在typename或class后面带上省略号,如下所示:

template <class... T>
void f(T... args);

省略号表示参数包T… args中可以包含0到任意个模板参数;获得参数包中每个参数即展开参数包;

[来源]

递归函数方式展开参数包

#include <iostream>
using namespace std;
//递归终止函数
void print()
{
   cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
   cout << "parameter " << head << endl;
   print(rest...);
}


int main(void)
{
   print(1,2,3,4);
   return 0;
}

求和实例:

template<typename T>
T sum(T t)
{
    return t;
}
template<typename T, typename ... Types>
T sum (T first, Types ... rest)
{
    return first + sum<T>(rest...);
}

sum(1,2,3,4); //10

逗号表达式展开参数包

template <class T>
void printarg(T t)
{
   cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
   int arr[] = {(printarg(args), 0)...};
}

expand(1,2,3,4);

上述 {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0),  etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)];

还可以进一步改进:

template<class F, class... Args>void expand(const F& f, Args&&...args) 
{
  //这里用到了完美转发
  initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
expand([](int i){cout<<i<<endl;}, 1,2,3);

使用C++14的新特性泛型lambda表达式:

expand([](auto i){cout<<i<<endl;}, 1,2.0,“test”);

与之相对应的,我们还可以拥有可变模板参数类(相比较上文的可变模板参数函数),C++11中的std::tuple即为一个可变模板类;

std::tuple<> tp;
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);

模版偏特化和递归方式来展开参数包

//前向声明
template<typename... Args>
struct Sum;

//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};

//递归终止
template<typename Last>
struct Sum<Last>
{
    enum { value = sizeof (Last) };
};

继承方式展开参数包

//整型序列的定义
template<int...>
struct IndexSeq{};

//继承方式,开始展开参数包
template<int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...> {};

// 模板特化,终止展开参数包的条件
template<int... Indexes>
struct MakeIndexes<0, Indexes...>
{
    typedefIndexSeq<Indexes...> type;
};

int main()
{
    using T = MakeIndexes<3>::type;
    cout <<typeid(T).name() << endl;
    return 0;
}

可变参数模版消除重复代码

template<typename…  Args>
T* Instance(Args&&… args)
{
    return new T(std::forward<Args>(args)…);
}
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);

可变模板类的展开比较麻烦,如果后续有较多的应用的话另开文详解;

23.推广的(非平凡)联合体

union与struct相似,但union所有成员占用同一块内存空间,其大小为最大的那个成员的大小;

在C++11中,union可以有成员函数,包括构造函数和析构函数,但不能继承,也不能被继承,也不能有虚函数;

可以有成员变量是类,但是这个类成员不能有自己定义的构造、拷贝、析构、赋值等函数;

不能有引用成员,不管是左值引用还是右值引用,不能有静态成员变量,但是可以有静态成员函数;

24.推广的POD

Plain Old Data,简旧数据,表示该类型与用于 C 程序语言的类型兼容,即它能直接以二进制形式与 C 库交互(C++20弃用,替换为诸如平凡类型(TrivialType));

文档里的内容很少,我找了一些其它内容的参考;

厘清几个术语,

POD是为了与C兼容而提出的概念,POD里的class可以与C中的结构体一致使用memcpy或memset等函数,POD的定义需要满足两个条件:①必须是平凡类型②必须是标准布局;

那么问题来了,平凡类型和标准布局又是什么呢?

平凡类型(TrivialType)有如下定义:

  • 拥有平凡的默认构造函数和析构函数。这里的平凡的默认构造函数是指在不定义构造函数的情况下,编译器默认生成的构造函数。如果自己定义了构造函数,但是什么都没有做,仍然不是平凡的构造函数,即使用=default定义的默认构造函数,下同;
  • 拥有平凡的拷贝构造函数和移动构造函数;
  • 拥有平凡的拷贝赋值函数和移动赋值函数;
  • 不能包含虚函数和虚基类;

标准布局有如下定义:

  • 所有非静态成员具有相同的访问权限;
  • 在继承树中最多只有一个类中有非静态数据成员;
  • 子类中的第一个非静态成员的类型与其基类不同;(这条GNU C++遵守但VC++不遵守)
  • 没有虚函数和虚基类;
  • 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局;

25.Unicode 字符串字面量

这一点和20相类似;

"s字符序列(可选)"(1)
L"s字符序列(可选)"(2)
u8"s字符序列(可选)"(3)(C++11 起)
u"s字符序列(可选)"(4)(C++11 起)
U"s字符序列(可选)"(5)(C++11 起)
前缀(可选) R"d字符序列(可选)(r字符序列(可选))d字符序列(可选)"(6)(C++11 起)
语法

在翻译阶段,两个字符串字面量可以进行拼接,如果二者字符串的前缀编码相同或一者有另一者无,则二者可拼接,反之非良构;

26.用户定义字面量

简单的说,就是允许用户为字面量自定义后缀以表示单位;

在过去,C++有整型、浮点型、字符、字符串四种字面量,如25所示,我们可以通过添加后缀来表示具体的类型如长整型(long)333l;

而如果我们现在有一个变量height=10;而从代码表面我们无法得知10指代的单位是厘米或是米还是其它,于是C++11允许用户自定义字面量后缀;

#include <string>
void operator "" _km(long double); // OK ,将为 1.0_km 所调用
std::string operator "" _i18n(const char*, std::size_t); // OK
template <char...> double operator "" _π(); // OK
float operator ""_e(const char*); // OK
 
float operator ""Z(const char*); // 错误:后缀必须以下划线开始
double operator"" _Z(long double); // 错误:所有以下划线后随大写字母开始的名称受到保留
double operator""_Z(long double); // OK:虽然 _Z 被保留,但允许 ""_Z
double operator ""_Z(const char* args); // OK:能重载字面量运算符
 
int main() {}

字面量运算符仅允许下列形参列表:

( const char * )(1)
( unsigned long long int )(2)
( long double )(3)
( char )(4)
( wchar_t )(5)
( char8_t )(6)(C++20 起)
( char16_t )(7)
( char32_t )(8)
( const char * , std::size_t )(9)
( const wchar_t * , std::size_t )(10)
( const char8_t * , std::size_t )(11)(C++20 起)
( const char16_t * , std::size_t )(12)
( const char32_t * , std::size_t )(13)

27.属性

为类型、对象、代码等引入由实现定义的属性。[[属性]][[属性1属性2属性3(实参)]][[命名空间::属性(实参)]]alignas-说明符

正式而言,语法是

[[ 属性列表 ]](C++11 起)
[[ using 属性命名空间 : 属性列表 ]](C++17 起)

C++ 标准仅定义下列属性

[[noreturn]](C++11)指示函数不返回
[[carries_dependency]](C++11)指示释放消费 std::memory_order 中的依赖链传入和传出该函数。
[[deprecated]](C++14)
[[deprecated("原因")]](C++14)
指示允许使用声明有此属性的名称或实体,但因 原因 而不鼓励使用。
[[fallthrough]](C++17)指示从前一 case 标号直落是有意的,而在发生直落时给出警告的编译器不应该为此诊断。
[[nodiscard]](C++17)
[[nodiscard("原因")]](C++20)
鼓励编译器在返回值被舍弃时发布警告。
[[maybe_unused]](C++17)压制编译器在未使用实体上的警告,若存在。
[[likely]](C++20)
[[unlikely]](C++20)
指示编译器应该针对通过某语句的执行路径比任何其他执行路径更可能或更不可能的情况进行优化。
[[no_unique_address]](C++20)指示非静态数据成员不需要拥有不同于其类的所有其他非静态数据成员的地址。
[[optimize_for_synchronized]](TM TS)指示应该针对来自 synchronized 语句的调用来优化该函数定义
[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]] [[nodiscard]]
inline int f(); // 声明 f 带四个属性
 
[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
int f(); // 同上,但使用含有四个属性的单个属性说明符
 
// C++17:
[[using gnu : const, always_inline, hot]] [[nodiscard]]
int f[[gnu::always_inline]](); // 属性可出现于多个说明符中
 
int f() { return 0; }
 
int main() {}

属性可以作用在代码中几乎所有的位置如函数、变量、类型、块等,但只有属性和修饰对象有关联时,属性才有意义,如[[noreturn]]适用于函数;

关于这方面的资料有点少,有时间针对各属性另开篇章测试;

28.lambda 表达式

语法

[ 捕获 ] ( 形参 ) lambda说明符 约束(可选) { 函数体 }(1)
[ 捕获 ] { 函数体 }(2)(C++23 前)
[ 捕获 ] lambda说明符 { 函数体 }(2)(C++23 起)
[ 捕获 ] <模板形参> 约束(可选)
( 形参 ) lambda说明符 约束(可选) { 函数体 }
(3)(C++20 起)
[ 捕获 ] <模板形参> 约束(可选) { 函数体 }(4)(C++20 起)
(C++23 前)
[ 捕获 ] <模板形参> 约束(可选) lambda说明符 { 函数体 }(4)(C++23 起)

这个应该很熟悉了,比较麻烦的是递归调用,在另一篇中有粗略介绍[链接];

而后在14、17、20的标准中有进一步的扩展,暂且不表;

捕获

标识符(1)
标识符 ...(2)
标识符 初始化器(3)(C++14 起)
& 标识符(4)
& 标识符 ...(5)
& 标识符 初始化器(6)(C++14 起)
this(7)
* this(8)(C++17 起)
... 标识符 初始化器(9)(C++20 起)
& ... 标识符 初始化器(10)(C++20 起)
struct S2 { void f(int i); };
void S2::f(int i)
{
    [&]{};          // OK:默认以引用捕获
    [&, i]{};       // OK:以引用捕获,但 i 以值捕获
    [&, &i] {};     // 错误:以引用捕获为默认时的以引用捕获
    [&, this] {};   // OK:等价于 [&]
    [&, this, i]{}; // OK:等价于 [&, i]
}
struct S2 { void f(int i); };
void S2::f(int i)
{
    [=]{};          // OK:默认以复制捕获
    [=, &i]{};      // OK:以复制捕获,但 i 以引用捕获
    [=, *this]{};   // C++17 前:错误:无效语法
                    // C++17 起:OK:以复制捕获外围的 S2
    [=, this] {};   // C++20 前:错误:= 为默认时的 this
                    // C++20 起:OK:同 [=]
}
struct S2 { void f(int i); };
void S2::f(int i)
{
    [i, i] {};        // 错误:i 重复
    [this, *this] {}; // 错误:"this" 重复 (C++17)
}

29.noexcept 说明符

用于指定函数是否抛出异常;

用于取代throw,throw作为动态异常说明在11中弃用,17中移除,而后17中由throw()取代,而后throw()在17中出于弃用状态,在20中移除;

C++17起,throw()被重定义为严格等价于noexcept(true);

// foo 是否声明为 noexcept 取决于 T() 是否会抛出异常
template <class T>
    void foo() noexcept(noexcept(T())) {}
 
void bar() noexcept(true) {}
void baz() noexcept { throw 42; }  // noexcept 等同于 noexcept(true)
 
int main() 
{
    foo<int>(); // noexcept(noexcept(int())) => noexcept(true),所以这是可以的
 
    bar(); // 可以
    baz(); // 能编译,但会在运行时调用 std::terminate
}

30.noexcept 运算符

noexcept 运算符进行编译时检查,如果表达式不会抛出任何异常则返回 true。它可用于函数模板的 noexcept 说明符中,以声明函数将对某些类型抛出异常,但不对其他类型抛出;

#include <iostream>
#include <utility>
#include <vector>
 
void may_throw();
void no_throw() noexcept;
auto lmay_throw = []{};
auto lno_throw = []() noexcept {};
 
class T
{
public:
    ~T(){} // 析构函数妨碍了移动构造函数
           // 复制构造函数不会抛出异常
};
 
class U
{
public:
    ~U(){} // 析构函数妨碍了移动构造函数
           // 复制构造函数可能会抛出异常
    std::vector<int> v;
};
 
class V
{
public:
    std::vector<int> v;
};
 
int main()
{
    T t;
    U u;
    V v;
 
    std::cout << std::boolalpha
        << "may_throw() 可能会抛出异常吗?" << !noexcept(may_throw()) << '\n'
        << "no_throw() 可能会抛出异常吗?" << !noexcept(no_throw()) << '\n'
        << "lmay_throw() 可能会抛出异常吗?" << !noexcept(lmay_throw()) << '\n'
        << "lno_throw() 可能会抛出异常吗?" << !noexcept(lno_throw()) << '\n'
        << "~T() 可能会抛出异常吗?" << !noexcept(std::declval<T>().~T()) << '\n'
        // 注:以下各项测试也要求 ~T() 不会抛出异常
        // 因为 noexcept 中的表达式会构造并销毁临时量
        << "T(T 右值) 可能会抛出异常吗?" << !noexcept(T(std::declval<T>())) << '\n'
        << "T(T 左值) 可能会抛出异常吗?" << !noexcept(T(t)) << '\n'
        << "U(U 右值) 可能会抛出异常吗?" << !noexcept(U(std::declval<U>())) << '\n'
        << "U(U 左值) 可能会抛出异常吗?" << !noexcept(U(u)) << '\n'  
        << "V(V 右值) 可能会抛出异常吗?" << !noexcept(V(std::declval<V>())) << '\n'
        << "V(V 左值) 可能会抛出异常吗?" << !noexcept(V(v)) << '\n';  
}

31.alignof 运算符 & alignas 说明符

返回 std::size_t 类型的值;

返回由类型标识所指示的类型的任何实例所要求的对齐字节数,该类型可以为完整对象类型、元素类型完整的数组类型或者到这些类型之一的引用类型。若类型为引用类型,则运算符返回被引用类型的对齐;若类型为数组类型,则返回元素类型的对齐要求;

#include <iostream>
 
struct Foo {
    int   i;
    float f;
    char  c;
};
 
// 注:下面的 `alignas(alignof(long double))` 如果需要可以简化为 
// `alignas(long double)`
struct alignas(alignof(long double)) Foo2 {
    // Foo2 成员的定义...
};
 
struct Empty {};
 
struct alignas(64) Empty64 {};
 
int main()
{
    std::cout << "对齐字节数"  "\n"
        "- char             :" << alignof(char)    << "\n"
        "- 指针             :" << alignof(int*)    << "\n"
        "- Foo 类           :" << alignof(Foo)     << "\n"
        "- Foo2 类          :" << alignof(Foo2)     << "\n"
        "- 空类             :" << alignof(Empty)   << "\n"
        "- alignas(64) Empty:" << alignof(Empty64) << "\n";
}

32.多线程内存模型

出于编译器优化或者指令流水线等原因,程序并非一定按照代码顺序执行,在单线程的情况下,这一点不会产生如何的影响,毕竟最终产生的结果是一致的,而在多线程中,并发不能保证修改和访问共享变量的操作原子性使得中间状态暴露或者变量修改后无法及时被另一个线程得知,故而需要进行同步,而解决同步问题的前提是确定内存模型,即确定线程间应该怎样通过共享内存来进行交互;

在计算机中,写操作的资源消耗要远大于读操作,因此往往会对写操作进行优化,于是,写操作通常会在cache中缓存,使得该操作可能无法及时被另一个CPU得知;

(应用见27中的[[carries_dependency]][链接],memory_order_consume)

C++11中定义了如下6种语义:

enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};

release&acquire

其中,release用于写操作,acquire用于读操作,在一个线程中对某内存进行release写,则在该线程release写之前的所有内存操作,在另一个线程进行acquire读后可以获得;

这对语义用于阻止乱序的发生,因为release操作之前的所有内存操作不允许被乱序到release之后,acquire 操作之后的所有内存操作不允许被乱序到acquire之前;

同时需要注意的是:

  • release和acquire必须配合使用,分开单独使用是没有意义
  • release只对写操作(store) 有效,对读 (load) 是没有意义的
  • acquire只对读操作有效,对写操作是没有意义的

acq_rel

当然有时候我们对某操作既需要release同时又需要acquire,于是C++11中定义了memory_order_acq_rel读写更新的原子操作;

此外acq_rel还有Memory Barrier(内存栅栏)的作用,如PPC中的sync;

seq_cst

全称sequential consistency,除了有release-acquire语义作用外,还有全局顺序要求的作用,即对所有以memory_order_seq_cst方式进行的内存操作,不管它们是不是分散在不同的 pu中同时进行,这些操作所产生的效果最终都要求有一个全局的顺序,而且这个顺序在各个相关的线程看起来是一致的;

简单的说,就是让多线程操作在宏观中的运行顺序如同单线程,在不确定内存顺序的情况下即可以缺省使用该语义;

releaxed

即无约定,允许CPU和编译器以任何形式进行优化;

consume

  • 对当前要读取的内存施加 release 语义,在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前
  • 在这个原子变量上施加 release 语义的操作发生之后,acquire 可以保证读到所有在 release 前发生的并且与这块内存有关的写入

这个语义相较上述稍显复杂,且在实际代码中无法使用该语义,在GCC中会自动将该内存序转换为std::memory_order_acquire,故而不多做介绍;

33.线程局部存储

程序中所有对象有如下四种存储期:automatic、static、thread、dynamic,其中thread便为C++11所新增;

这类对象的存储在线程开始时分配,并在线程结束时解分配。每个线程拥有它自身的对象实例。只有声明为 thread_local 的对象拥有此存储期。 thread_local 能与 static 或 extern 一同出现,它们用于调整链接;

[来源]

// thread_local.cpp
#include <iostream>
#include <thread>

class A {
public:
  A() {
    std::cout << std::this_thread::get_id()
              << " " << __FUNCTION__
              << "(" << (void *)this << ")"
              << std::endl;
  }

  ~A() {
    std::cout << std::this_thread::get_id()
              << " " << __FUNCTION__
              << "(" << (void *)this << ")"
              << std::endl;
  }

  // 线程中,第一次使用前初始化
  void doSth() {
  }
};

thread_local A a;

int main() {
  a.doSth();
  std::thread t([]() {
    std::cout << "Thread: "
              << std::this_thread::get_id()
              << " entered" << std::endl;
    a.doSth();
  });

  t.join();

  return 0;
}

34.GC接口

该部分所有内容在C++23中被移除……

定义于头文件 <memory>
declare_reachable(C++11)(C++23 中移除)声明一个对象不能被回收
(函数)
undeclare_reachable(C++11)(C++23 中移除)声明一个对象可以被回收
(函数模板)
declare_no_pointers(C++11)(C++23 中移除)声明该内存区域不含可追踪指针
(函数)
undeclare_no_pointers(C++11)(C++23 中移除)取消 std::declare_no_pointers 的效果
(函数)
pointer_safety(C++11)(C++23 中移除)列出指针安全模式
(枚举)
get_pointer_safety(C++11)(C++23 中移除)返回当前的指针安全模式
(函数)
垃圾收集器支持

35.基于范围的 for 循环

再熟悉不过了的C++11特性,在后续的几个标准中有做扩展;

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> v = {0, 1, 2, 3, 4, 5};
 
    for (const int& i : v) // 以 const 引用访问
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (auto i : v) // 以值访问,i 的类型是 int
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (auto&& i : v) // 以转发引用访问,i 的类型是 int&
        std::cout << i << ' ';
    std::cout << '\n';
 
    const auto& cv = v;
 
    for (auto&& i : cv) // 以转发引用访问,i 的类型是 const int&
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (int n : {0, 1, 2, 3, 4, 5}) // 初始化器可以是花括号初始化器列表
        std::cout << n << ' ';
    std::cout << '\n';
 
    int a[] = {0, 1, 2, 3, 4, 5};
    for (int n : a) // 初始化器可以是数组
        std::cout << n << ' ';
    std::cout << '\n';
 
    for ([[maybe_unused]] int n : a)  
        std::cout << 1 << ' '; // 不必使用循环变量
    std::cout << '\n';
 
    for (auto n = v.size(); auto i : v) // 初始化语句(C++20)
        std::cout << --n + i << ' ';
    std::cout << '\n';
 
    for (typedef decltype(v)::value_type elem_t; elem_t i : v)
    // typedef 声明作为初始化语句(C++20)
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (using elem_t = decltype(v)::value_type; elem_t i : v)
    // 别名声明作为初始化语句,同上(C++23)
        std::cout << i << ' ';
    std::cout << '\n';
}

36.static_assert 声明

在编译时进行断言检查;

static_assert ( 布尔常量表达式 , 消息 )

#include <type_traits>
 
template <class T>
void swap(T& a, T& b) noexcept
{
    static_assert(std::is_copy_constructible<T>::value,
                  "交换需要可复制");
    static_assert(std::is_nothrow_copy_constructible<T>::value
               && std::is_nothrow_copy_assignable<T>::value,
                  "交换需要可复制及可赋值,且无异常抛出");
    auto c = b;
    b = a;
    a = c;
}
 
template <class T>
struct data_structure
{
    static_assert(std::is_default_constructible<T>::value,
                  "数据结构要求元素可默认构造");
};
 
struct no_copy
{
    no_copy ( const no_copy& ) = delete;
    no_copy () = default;
};
 
struct no_default
{
    no_default () = delete;
};
 
int main()
{
    int a, b;
    swap(a, b);
 
    no_copy nc_a, nc_b;
    swap(nc_a, nc_b); // 1
 
    data_structure<int> ds_ok;
    data_structure<no_default> ds_error; // 2
}

因为断言assert宏只有在程序运行时起作用,而#error只有在编译器预处理时起作用,故而便有了static_assert以在编译时进行断言;

37.emplace_back

实现功能与push_back类似,但其内作用原理不同;

前者通过调用构造函数和复制构造函数来对容器添加一个数值,即先建立一个临时对象,而后将这个临时对象添加到容器的末尾;

而后者则是直接在容器尾部增加一个元素,即只调用构造而不调用拷贝;

上一篇
下一篇