240728-至今 C++ 特殊知识点记忆

266 阅读4分钟

本文记录了我学 C++ 以来,需要特别记忆的一些特殊知识点

记录

  1. 《C++ Core Guidelines》—— 偏好 {} 初始化语法
    • Use = only when you are sure that there can be no narrowing conversions. For built-in arithmetic types, use = only with auto.
    • ={} gives copy initialization whereas {} gives direct initialization. Like the distinction between copy-initialization and direct-initialization itself, this can lead to surprises. {} accepts explicit constructors; ={} does not.
  2. 《Hacking C++》—— explicit 关键字与隐式转换
    • Make user-defined constructors explicit by default!
  3. 《C++ Core Guidelines》—— SF.8:为所有头文件使用 #include 守卫
    • 避免文件被多次 #include。多次 #include 很有可能导致重定义错误。为了避免 include 守卫冲突,不要仅仅以文件名来命名守卫。请确保还包括一个关键字和良好的区分符,例如头文件所属的库或组件的名称。
    • 示例:
    // file foobar.h:
    #ifndef LIBRARY_FOOBAR_H
    #define LIBRARY_FOOBAR_H
    // ... declarations ...
    #endif // LIBRARY_FOOBAR_H
    
    • 注意:有些 C++ 实现提供了额外的扩展,比如 #pragma once 作为 include 守卫的替代方案。它不是标准的,并且不可移植。 它将宿主机器的文件系统语义注入到你的程序中,除此之外还将你锁定到某个特定编译器。我们的建议是使用 ISO C++ 编写:参见 rule P.2.

    但是其实 UE5 引擎和 Piccolo 引擎的源码里都用了 #pragma once

  4. 《C++ Core Guidelines》—— SF.7:不要在头文件的全局范围内使用 using namespace
    • 这样做会使 #include 的使用者(.cpp 文件)无法有效地消除歧义和使用替代方案。这也使得 #include 的头文件变得依赖于包含顺序,因为它们在不同的顺序中包含时可能会有不同的含义。
  5. 《C++ Core Guidelines》—— ES.41: 如果对运算符优先级有疑问,请使用括号
    • 避免错误。可读性。并不是每个人都记得运算符优先级表。我们建议程序员记住算术运算符和逻辑运算符的优先级表,但在与其他运算符混用位逻辑运算符时考虑使用括号。
  6. 《C++ Core Guidelines》—— ES.101: 使用无符号类型进行位操作
    • 无符号类型支持位操作而不会因符号位引起意外。
  7. 《C++ Core Guidelines》—— ES.102: 使用有符号类型进行算术运算
    • 因为大多数算术运算假设为有符号;例如,当 y > x 时,x - y 会产生负数。如果你没有预期到,无符号算术运算可能会产生意外的结果。对于混合有符号和无符号算术运算,这一点更为明显。例子:
    • 示例:
      template<typename T, typename T2>
      T subtract(T x, T2 y)
      {
          return x - y;
      }
      
      void test()
      {
          int s = 5;
          unsigned int us = 5;
          cout << subtract(s, 7) << '\n';       // -2
          cout << subtract(us, 7u) << '\n';     // 4294967294
          cout << subtract(s, 7u) << '\n';      // -2
          cout << subtract(us, 7) << '\n';      // 4294967294
          cout << subtract(s, us + 2) << '\n';  // -2
          cout << subtract(us, s + 2) << '\n';  // 4294967294
      }
      
    • 如果你确实需要模运算,请使用无符号类型,并在必要时添加注释。
    • 标准库使用无符号类型作为下标。内置数组使用有符号类型作为下标。这会导致意外(和错误)不可避免。
  8. 《C++ Core Guidelines》—— ES.107: 不要使用无符号类型作为下标,优先使用 gsl::index
    • 理由:避免有符号/无符号的混淆;实现更好的优化;实现更好的错误检测;避免auto和int带来的陷阱。
    • 内置数组允许有符号下标。标准库容器使用无符号下标。因此,不可能有完美且完全兼容的解决方案(除非标准库容器在未来某天改为使用有符号下标)。鉴于无符号和有符号/无符号混合的问题,最好坚持使用大小足够的(有符号)整数,而 gsl::index 可以保证这一点。

    由于 gsl 库并不是标准库之一,我们可以用 ptrdiff_t 来替代 gsl::index

    • 错误用法示例:
      vector<int> vec = /*...*/;
      
      for (int i = 0; i < vec.size(); i += 2)                    // 可能不够大
          cout << vec[i] << '\n';
      for (unsigned i = 0; i < vec.size(); i += 2)               // 存在回绕风险
          cout << vec[i] << '\n';
      for (auto i = 0; i < vec.size(); i += 2)                   // 可能不够大
          cout << vec[i] << '\n';
      for (vector<int>::size_type i = 0; i < vec.size(); i += 2) // 过于冗长
          cout << vec[i] << '\n';
      for (auto i = vec.size()-1; i >= 0; i -= 2)                // 错误
          cout << vec[i] << '\n';
      for (int i = vec.size()-1; i >= 0; i -= 2)                 // 可能不够大
          cout << vec[i] << '\n';
      ···
      
    • 正确用法示例:
      vector<int> vec = /*...*/;
      
      for (gsl::index i = 0; i < vec.size(); i += 2)             // 正确
          cout << vec[i] << '\n';
      for (gsl::index i = vec.size()-1; i >= 0; i -= 2)          // 正确
          cout << vec[i] << '\n';
      ···
      
  9. 关于枚举——《Hacking C++》关于枚举——《C++ Core Guidelines》
    • 《Hacking C++》: avoid unscoped enumerations
    • 《Hacking C++》: if you set enumerator values explicitly, do it for all enumerators
    • 《C++ Core Guidelines》: Enum.7: Specify the underlying type of an enumeration only when necessary.
    • 《C++ Core Guidelines》: Enum.8: Specify enumerator values only when necessary.
  10. 关于函数的 [[nodiscard]] 属性和 noexcept 关键字
    • 《Hacking C++》: [[nodiscard]] encourages compilers to issue warnings if function return values are discarded. Declare your function return values  [[nodiscard]]
      • if calling it without using the return value makes no sense in any situation
      • if users could be confused about its purpose, if the return value is ignored
    • 《Hacking C++》: The noexcept keyword specifies that a function promises to never throw exceptions / let exceptions escape. If an exception escapes from a noexcept function anyway, the program will be aborted.
      •  void foo () noexcept { … }
        
    • 《C++ Core Guidelines》:E.12: Use noexcept when exiting a function because of a throw is impossible or unacceptable