跳转至

保留字

  • 保留字是一种更广泛的概念,它包括所有不能被程序员用作标识符(变量名、函数名等)的单词,不仅包括关键字,还包括将来可能被引入为关键字的词汇。
  • 保留字的目的是为了预留这些单词,以便在将来的 C++ 标准中使用,而当前的标准版本中可能没有特定的语法或语义与之相关联。

1 关键字

1.1 auto

  • 功能:自动推导类型

1.1.1 推导规则

  • 规则 1:如果 auto 声明的变量是按值初始化,则推导出的类型会忽略 cv 限定符。进一步解释为,在使用 auto 声明变量时,既没有使用引用,也没有使用指针,那么编译器在推导的时候会忽略 const 和 volatile 限定符。当然 auto 本身也支持添加 cv 限定符:
const int i = 5;
auto j = i; // auto推导类型为int,而非const int
auto &m = i; // auto推导类型为const int,m推导类型为const int&
auto *k = i; // auto推导类型为const int,k推导类型为const int*
const auto n = j; // auto推导类型为int,n的类型为const int

根据规则 1,在上面的代码中,虽然 i 是 const int 类型,但是因为按值初始化会忽略 cv 限定符,所以 j 的推导类型是 int 而不是 const int。而 m 和 k 分别按引用和指针初始化,因此其 cv 属性保留了下来。另外,可以用 const 结合 auto,让 n 的类型推导为 constint。 - 规则 2:使用 auto 声明变量初始化时,目标对象如果是引用,则引用属性会被忽略:

int i = 5;
int &j = i;
auto m = j; // auto推导类型为int,而非int&

根据规则 2,虽然 j 是 i 的引用,类型为 int&,但是在推导 m 的时候会忽略其引用。 - 规则 3:使用 auto 和万能引用声明变量时,对于左值会将 auto 推导为引用类型:

int i = 5;
auto&& m = i; // auto推导类型为int& (这里涉及引用折叠的概念)
auto&& j = 5; // auto推导类型为int

根据规则 3,因为 i 是一个左值,所以 m 的类型被推导为 int&,auto 被推导为 int&,这其中用到了引用折叠的规则。而 5 是一个右值,因此 j 的类型被推导为 int&&,auto 被推导为 int。 - 规则 4:使用 auto 声明变量,如果目标对象是一个数组或者函数,则 auto 会被推导为对应的指针类型:

int i[5];
auto m = i; // auto推导类型为int*
int sum(int a1, int a2)
{
  return a1+a2;
}
auto j = sum // auto推导类型为int (__cdecl *)(int,int)

根据规则 4,虽然 i 是数组类型,但是 m 会被推导退化为指针类型,同样,j 也退化为函数指针类型。 - 规则 5:当 auto 关键字与列表初始化组合时,这里的规则有新老两个版本,这里只介绍新规则(C++17 标准)。 (1)直接使用列表初始化,列表中必须为单元素,否则无法编译,auto 类型被推导为单元素的类型。 (2)用等号加列表初始化,列表中可以包含单个或者多个元素,auto 类型被推导为 std::initializer_list<T>,其中 T 是元素类型。请注意,在列表中包含多个元素的时候,元素的类型必须相同,否则编译器会报错。

auto x1 = { 1, 2 }; // x1类型为 std::initializer_list<int>
auto x2 = { 1, 2.0 }; // 编译失败,花括号中元素类型不同
auto x3{ 1, 2 }; // 编译失败,不是单个元素
auto x4 = { 3 }; // x4类型为std::initializer_list<int>
auto x5{ 3 }; // x5类型为int

在上面的代码中,x1 根据规则 5(2)被推导为 std::initializer_list<T>,其中的元素都是 int 类型,所以 x1 被推导为 std::initializer_list<int>。同样,x2 也应该被推导为 std::initializer_list<T>,但是显然两个元素类型不同,导致编译器无法确定 T 的类型,所以编译失败。根据规则 5(1),x3 包含多个元素,直接导致编译失败。x4 和 x1 一样被推导为 std::initializer_ list<int>,x5 被推导为单元素的类型 int。 根据上面这些规则,读者可以思考下面的代码,auto 会被推导成什么类型呢?

class Base {
public:
  virtual void f()
  {
    std::cout << "Base::f()" << std::endl;
  };
};
class Derived : public Base {
public:
  virtual void f() override
  {
    std::cout << "Derived::f()" << std::endl;
  };
};
Base* d = new Derived();
auto b = *d;
b.f();

以上代码有 Derived 和 Base 之间的继承关系,并且 Derived 重写了 Base 的 f 函数。代码使用 new 创建了一个 Derived 对象,并赋值于基类的指针类型变量上。读者知道 d->f() 一定调用的是 Derived 的 f 函数。但是 b.f() 调用的又是谁的 f 函数呢?实际上,由于 autob = *d 这一句代码是按值赋值的,因此 auto 会直接推导为 Base。代码自然会调用 Base 的复制构造函数,也就是说 Derived 被切割成了 Base,这里的 b.f() 最终调用 Base 的 f 函数。

1.1.2 返回类型推导 (C++14)

auto sum(int a1, int a2) { return a1 + a2; }

当有分支时,每一个分支的返回类型需要相同,否则编译失败,如下示例编译失败

auto sum(long a1, long a2)  
{  
  if (a1 < 0) {  
    return 0; // 返回int类型  
  }  
  else {  
    return a1 + a2; // 返回long类型  
  }  
}

1.1.3 auto 用于泛型 lambda(C++14)

lambda 表达式具备了模版函数的能力,我们称它为泛型 lambda 表达式。不需要使用 template 关键字,函数参数使用 auto 占位符即可。

  • 示例
int main()  
{  
  auto foo = [](auto a) { return a; };  
  int three = foo(3);  
  char const* hello = foo("hello");  
}

在上面的代码中 a1 被推导为 int 类型,a2 被推导为 double 类型,返回值 retval 被推导为 double 类型。 lambda 表达式返回 auto 引用的方法:

auto l = [](int &i)->auto& { return i; };  
auto x1 = 5;  
auto &x2 = l(x1);  
assert(&x1 == &x2); // 有相同的内存地

1.1.4 非类型模板形参占位符(C++17)

#include <iostream>  
template<auto N>  
void f()  
{  
  std::cout << N << std::endl;  
}  
int main()  
{  
  f<5>(); // N为int类型  
  f<'c'>(); // N为char类型  
  f<5.0>(); // 编译失败,模板参数不能为double  
}

在上面的代码中,函数 f<5>() 中 5 的类型为 int,所以 auto 被推导为 int 类型。同理,f<'c'>() 的 auto 被推导为 char 类型。由于 f<5.0>() 的 5.0 被推导为 double 类型,但是模板参数不能为 double 类型,因此导致编译失败。

1.2 delctype

  • 功能:通过表达式或对象来识别类型
  • 语法
  • decltype ( 实体 ) (1) (C++11 起)
  • decltype ( 表达式 ) (2) (C++11 起)
  • 推导规则
注意如果对象的名字带有括号,那么它会被当做通常的左值表达式,从而 decltype(x) 和 decltype((x)) 通常是不同的类型。
1) 如果实参是没有括号的标识表达式或没有括号的类成员访问表达式,那么 decltype 产生以该表达式命名的实体的类型。如果没有这种实体或该实参指名了一组重载函数,那么程序非良构。
  如果实参是指名某个结构化绑定的没有括号的标识表达式,那么 decltype 产生其被引用类型(在关于结构化绑定声明的说明中有所描述)。(C++17 起)
  如果实参是指名某个非类型模板形参的没有括号的标识表达式,那么 decltype 生成该模板形参的类型(当该模板形参以占位符类型声明时,类型会先进行任何所需的类型推导)。(C++20 起)
2) 如果实参是其他类型为 T 的任何表达式,且
  a) 如果 表达式 的值类别是亡值,将会 decltype 产生 T&&;
  b) 如果 表达式 的值类别是左值,将会 decltype 产生 T&;
  c) 如果 表达式 的值类别是纯右值,将会 decltype 产生 T。
    如果 表达式 是返回类类型纯右值的函数调用,或是右操作数为这种函数调用的逗号表达式,那么不会对该纯右值引入临时量。(C++17 前)
    如果 表达式 是除了(可带括号的)立即调用以外的 (C++20 起)纯右值,那么不会从该纯右值实质化临时对象:即这种纯右值没有结果对象。(C++17 起)
    该类型不需要是完整类型或拥有可用的析构函数,而且类型可以是抽象的。此规则不适用于其子表达式:decltype(f(g())) 中,g() 必须有完整类型,但 f() 不必。
  • 示例
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;
}

C++11 返回类型推导

template<class T1, class T2>  
auto sum(T1 a1, T2 a2)->decltype(a1 + a2)  
{  
  return a1 + a2;  
}  
auto x4 = sum(5, 10.5);

在 C++14 后可以直接使用 auto 推导返回值了

template<class T1, class T2>  
auto sum(T1 a1, T2 a2)  
{  
  return a1 + a2;  
}  
auto x5 = sum(5, 10.5);

1.3 constexpr

  • 功能:在编译期计算函数或表达式的值
  • 使用场景:如果能在编译期就能确定的值,可以使用 constepr。
  • 条件
  • 其表达式或函数的参数必须是字面量值,比如数值 1,2,3,或字符串“1234”
  • 表达式或函数参数也是 constexpr 表达式。
  • 示例
#include <iostream>
#include <stdexcept>

// C++11 constexpr 函数使用递归而非迭代
// (C++14 constexpr 函数可使用局部变量和循环)
constexpr int factorial(int n)
{
    return n <= 1? 1 : (n * factorial(n - 1));
}

// 字面类
class conststr {
    const char* p;
    std::size_t sz;
public:
    template<std::size_t N>
    constexpr conststr(const char(&a)[N]): p(a), sz(N - 1) {}

    // constexpr 函数通过抛异常来提示错误
    // C++11 中,它们必须用条件运算符 ?: 这么做
    constexpr char operator[](std::size_t n) const
    {
        return n < sz ? p[n] : throw std::out_of_range("");
    }
    constexpr std::size_t size() const { return sz; }
};

// C++11 constexpr 函数必须把一切放在单条 return 语句中
// (C++14 无此要求)
constexpr std::size_t countlower(conststr s, std::size_t n = 0,
                                             std::size_t c = 0)
{
    return n == s.size() ? c :
           'a' <= s[n] && s[n] <= 'z' ? countlower(s, n + 1, c + 1) :
                                        countlower(s, n + 1, c);
}

// 输出要求编译时常量的函数,用于测试
template<int n>
struct constN
{
    constN() { std::cout << n << '\n'; }
};

int main()
{
    std::cout << "4! = " ;
    constN<factorial(4)> out1; // 在编译时计算

    volatile int k = 8; // 不允许使用 volatile 者优化
    std::cout << k << "! = " << factorial(k) << '\n'; // 运行时计算

    std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
    constN<countlower("Hello, world!")> out2; // 隐式转换到 conststr
}

1.4 mutable

  • 功能:使定义为 const 的对象内的成员能被修改
  • 使用场景:如果 A 中有一个成员 m 被声明为 mutable,当 const A a; 时,使 a.m=value 能执行
  • 示例
int main()
{
    int n1 = 0;           // 非 const 对象
    const int n2 = 0;     // const 对象
    int const n3 = 0;     // const 对象(同 n2)
    volatile int n4 = 0;  // volatile 对象
    const struct
    {
        int n1;
        mutable int n2;
    } x = {0, 0};      // 带 mutable 成员的 const 对象

    n1 = 1; // ok,可修改对象
//  n2 = 2; // 错误:不可修改对象
    n4 = 3; // ok,当做副效应
//  x.n1 = 4; // 错误:const 对象的成员为 const
    x.n2 = 4; // ok,const 对象的 mutable 成员不是 const

    const int& r1 = n1; // 绑定到非 const 对象的 const 引用
//  r1 = 2; // 错误:试图通过到 const 的引用修改
    const_cast<int&>(r1) = 2; // ok,修改非 const 对象 n1

    const int& r2 = n2; // 绑定到 const 对象的 const 引用
//  r2 = 2; // 错误:试图通过到 const 的引用修改
//  const_cast<int&>(r2) = 2; // 未定义行为:试图修改 const 对象 n2
}

1.5 volatile

  • 功能:禁止编译器的优化
  • 使用场景:
  • static volatile 对象模仿映射于内存的 I/O 端口,而 static const volatile 对象模仿映射于内存的输入端口
  • sig_atomic_t 类型的 static volatile 对象用于与 signal 处理交流
  • 含有对 setjmp 宏调用的函数中的局部 volatile 变量,是在 longjmp 返回后仅有保证恢复其值的局部变量
  • volatile 变量可用于禁用某些优化形式,例如禁用死存储消除,或为微基准禁用常量折叠
  • 示例
#include <stdio.h>
#include <time.h>

int main(void)
{
    clock_t t = clock();
    double d = 0.0;
    for (int n = 0; n < 10000; ++n)
        for (int m = 0; m < 10000; ++m)
            d += d * n * m; // 读写非 volatile 对象
    printf("Modified a non-volatile variable 100m times. "
           "Time used: %.2f seconds\n",
           (double)(clock() - t)/CLOCKS_PER_SEC);

    t = clock();
    volatile double vd = 0.0;
    for (int n = 0; n < 10000; ++n)
        for (int m = 0; m < 10000; ++m) {
            double prod = vd * n * m; // 读 volatile 对象
            vd += prod; // 读写 volatile 对象
        } 
    printf("Modified a volatile variable 100m times. "
           "Time used: %.2f seconds\n",
           (double)(clock() - t)/CLOCKS_PER_SEC);
}
//可能的输出
//Modified a non-volatile variable 100m times. Time used: 0.00 seconds
//Modified a volatile variable 100m times. Time used: 0.79 seconds

1.6 const volatile

  • 功能:禁止由代码中改变值(允许由外部条件,如硬件改变),同时声明 volatile 禁止编译器的优化(即每一次读取数据都要原内存地址去读)
  • 使用场景:不允许由程序员通过代码去改变值,但可能由硬件等外部改变的值,因此又不能让编译器去优化,否则会导致读出的值是未更新的原始值。

1.7 noexcept

  • 功能:
  • 作为运算符 noexcept( 表达式 ):运算符进行编译时检查,若表达式声明为不抛出任何异常则返回 true
  • 作为说明符:指定函数是否抛出异常
  • 示例
#include <iostream>
#include <utility>
#include <vector>

void may_throw();
void no_throw() noexcept;//说明符
auto lmay_throw = []{};
auto lno_throw = []() noexcept {};
class T{
public:
  ~T(){} // 析构函数妨碍了移动构造函数
         // 复制构造函数为 noexcept
};
class U{
public:
  ~U(){} // 析构函数妨碍了移动构造函数
         // 复制构造函数为 noexcept(false)
  std::vector<int> v;
};
class V{
public:
  std::vector<int> v;
};

int main()
{
 T t;
 U u;
 V v;
 //以下noexcept作为运算符
 std::cout << std::boolalpha
           << "Is may_throw() noexcept? " << noexcept(may_throw()) << '\n'
           << "Is no_throw() noexcept? " << noexcept(no_throw()) << '\n'
           << "Is lmay_throw() noexcept? " << noexcept(lmay_throw()) << '\n'
           << "Is lno_throw() noexcept? " << noexcept(lno_throw()) << '\n'
           << "Is ~T() noexcept? " << noexcept(std::declval<T>().~T()) << '\n'
           // 注:以下各项测试也要求 ~T() 为 noexcept
           // 因为 noexccept 中的表达式构造并销毁了临时量
           << "Is T(rvalue T) noexcept? " << noexcept(T(std::declval<T>())) << '\n'
           << "Is T(lvalue T) noexcept? " << noexcept(T(t)) << '\n'
           << "Is U(rvalue U) noexcept? " << noexcept(U(std::declval<U>())) << '\n'
           << "Is U(lvalue U) noexcept? " << noexcept(U(u)) << '\n'  
           << "Is V(rvalue V) noexcept? " << noexcept(V(std::declval<V>())) << '\n'
           << "Is V(lvalue V) noexcept? " << noexcept(V(v)) << '\n';  
}

1.8 operator

  • 功能
  • 重载运算符
  • 用户定义转换函数
  • 使用场景
  • 当需要对类中某一运算符特殊处理时
  • 当需要对类进行类型转换时,比如,类型 A 转类型 B
  • 注意
  • 当重载运算符时,返回值类型在关键字 operator 之前;当进行类型转换时,返回类型在 operator 关键字之后。
  • 示例
class A 
{
public:
    A() :m_a(0) {}
    A(const int& a) { m_a = a; }
    //用户定义转换函数 int转换函数
    operator int () {
        return this->m_a;
    }
    //or
    //operator int& () {
    //    return this->m_a;
    //}
    operator int*() {
        return &this->m_a;
    }

    //重载 取地址符&
    A* operator &() {
        return this;
    }
private:
    int m_a;
};
int main()
{
    A a(2);
    //int转换函数
    int i = (int)(a);
    i = static_cast<int>(a);
    //int*转换函数
    int* pi = (int*)(a);
    //使用重载运算符&
    A* pa = &a;
}

2 说明符

2.1 final

  • 功能:
  • 限制 虚函数 不能在派生类中重写
  • 限制类不能被继承
  • 示例
struct Base
{
    virtual void foo();
};

struct A : Base
{
    void foo() final; // Base::foo is overridden and A::foo is the final override
    void bar() final; // Error: bar cannot be final as it is non-virtual
};

struct B final : A // struct B is final
{
    void foo() override; // Error: foo cannot be overridden as it is final in A
};

struct C : B // Error: B is final
{
};

2.2 override

  • 功能:指示 虚函数 重写自基类 虚函数
  • 示例
struct A
{
    virtual void foo();
    void bar();
};
struct B : A
{
    void foo() const override; // Error: B::foo does not override A::foo
                               // (signature mismatch)
    void foo() override; // OK: B::foo overrides A::foo
    void bar() override; // Error: A::bar is not virtual
};

2.3 alignas 和 alignof

  • 功能:
  • alignas 指定内存对齐字节数
  • alignof 计算内存对齐字节数 (注:alignof 不是说明符,是运算符)
  • 示例
struct alignas(128) G1 {
    float f;
};
struct G2
{
    char c1;
    G1 g;
    char c2;
};
int main()
{
    printf("sizeof(G1)=%d  alignof(G1)=%d\n", sizeof(G1), alignof(G1));
    printf("sizeof(G2)=%d  alignof(G2)=%d\n", sizeof(G2), alignof(G2));
    return 0;
}
//输出结果
//sizeof(G1)=128  alignof(G1)=128
//sizeof(G2)=384  alignof(G2)=128
  • 上例中 G1 和 G2 结构的内存布局
1>class G1  size(128):
1>  +---
1> 0    | f
1>  +---
1>class G2  size(384):
1>  +---
1> 0    | c1
1>      | <alignment member> (size=127)
1>128   | G1 g
1>256   | c2
1>      | <alignment member> (size=127)
1>  +---

2.4 explicit

  • 功能:使构造函数或转换函数为显式,即它不能用于隐式转换和复制初始化。
  • 示例
struct A
{
    A(int) { }      // 转换构造函数
    A(int, int) { } // 转换构造函数 (C++11)
    operator bool() const { return true; }
    A& operator =(int) { return *this; }
};

struct B
{
    explicit B(int) { }
    explicit B(int, int) { }
    explicit operator bool() const { return true; }
};

int main()
{
    A a1 = 1;      // OK:复制初始化选择 A::A(int)
    A a2(2);       // OK:直接初始化选择 A::A(int)
    A a3{ 4, 5 };   // OK:直接列表初始化选择 A::A(int, int)
    A a4 = { 4, 5 }; // OK:复制列表初始化选择 A::A(int, int)
    A a5 = (A)1;   // OK:显式转型进行 static_cast
    if (a1);      // OK:A::operator bool()
    bool na1 = a1; // OK:复制初始化选择 A::operator bool()
    bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化

//  B b1 = 1;      // 错误:复制初始化不考虑 B::B(int)
    B b2(2);       // OK:直接初始化选择 B::B(int)
    B b3{ 4, 5 };   // OK:直接列表初始化选择 B::B(int, int)
//  B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int,int)
    B b5 = (B)1;   // OK:显式转型进行 static_cast
    if (b2);      // OK:B::operator bool()
//  bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool()
    bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}
  • explicit+ 常量表达式 (C++20)
  • 示例
//c++20前
template<class T>
struct wrapper {
  template<class U, std::enable_if_t<std::is_convertible_v<U, T>>* = nullptr>
  wrapper(U const& u) : t_(u) {}

  template<class U, std::enable_if_t<!std::is_convertible_v<U, T>>* = nullptr>
  explicit wrapper(U const& u) : t_(u) {}

  T t_;
};
//C++20后
template<class T> 
struct wrapper { 
  template<class U> 
  explicit(!std::is_convertible_v<U, T>) 
  wrapper(U const& u) : t_(u) {} 

  T t_; 
};