室内设计做效果图可以接单的网站,医疗器械注册证,安卓优化大师全部版本,装饰设计乙级资质编码风格之(8)C特性规范(Google风格)3 Author: Once Day Date: 2024年10月12日 一位热衷于Linux学习和开发的菜鸟#xff0c;试图谱写一场冒险之旅#xff0c;也许终点只是一场白日梦… 漫漫长路#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: 源码分析_Once-Day的… 编码风格之(8)C特性规范(Google风格)3 Author: Once Day Date: 2024年10月12日 一位热衷于Linux学习和开发的菜鸟试图谱写一场冒险之旅也许终点只是一场白日梦… 漫漫长路有人对你微笑过嘛… 全系列文章可参考专栏: 源码分析_Once-Day的博客-CSDN博客 参考文章: Google C Style 文章目录 编码风格之(8)C特性规范(Google风格)31. 高级特性1.1 所有权和智能指针1.2 右值引用1.3 友元1.4 异常1.5 无异常1.6 运行时类型信息1.7 强制转换1.8 IO流1.9 预增和预减1.10 使用常量1.11 常量使用1.12 整数类型1.13 浮点数1.14 架构特性1.15 预处理器宏1.16 0和nullptr(NULL)1.17 sizeof获取大小1.18 类型推断(包括自动)1.19 类模板参数推导1.20 指定初始化器1.21 Lambda 表达式1.22 模板元编程1.23 概念和约束 1. 高级特性
1.1 所有权和智能指针
对于动态分配的对象最好有单一、固定的所有者。最好使用智能指针转移所有权。
“所有权”是一种用于管理动态分配内存(和其他资源)的簿记技术。动态分配对象的所有者是负责确保在不再需要时删除该对象或函数的对象或函数。所有权有时可以共享在这种情况下最后一个所有者通常负责删除它。即使所有权不共享也可以从一段代码转移到另一段代码。
“智能”指针是像指针一样工作的类例如通过重载 * 和 - 运算符。一些智能指针类型可用于自动化所有权簿记以确保满足这些责任。std::unique_ptr 是一种智能指针类型它表示动态分配对象的独占所有权当 std::unique_ptr 超出范围时该对象将被删除。它不能被复制但可以移动以表示所有权转移。std::shared_ptr 是一种智能指针类型它表示动态分配对象的共享所有权。std::shared_ptrs 可以被复制对象的所有权在所有副本之间共享并且当最后一个 std::shared_ptr 被销毁时该对象将被删除。
优点:
如果没有某种所有权逻辑几乎不可能管理动态分配的内存。转移对象的所有权可能比复制它更便宜(如果可以复制的话)。转移所有权可能比“借用”指针或引用更简单因为它减少了在两个用户之间协调对象生命周期的需要。智能指针可以通过使所有权逻辑明确、自文档化和无歧义来提高可读性。智能指针可以消除手动所有权簿记简化代码并排除大量错误。对于 const 对象共享所有权可以成为深度复制的简单而有效的替代方案。
缺点:
所有权必须通过指针(无论是智能指针还是普通指针)来表示和转移。指针语义比值语义更复杂尤其是在 API 中您不仅要担心所有权还要担心别名、生命周期和可变性等问题。值语义的性能成本通常被高估因此所有权转移的性能优势可能无法证明可读性和复杂性成本的合理性。转移所有权的 API 会强制其客户端采用单一内存管理模型。使用智能指针的代码对资源释放发生的位置不太明确。std::unique_ptr 使用移动语义来表达所有权转移这相对较新可能会让一些程序员感到困惑。共享所有权可能是精心所有权设计的一种诱人的替代方案会使系统的设计变得模糊不清。共享所有权需要在运行时进行显式记账这可能会很昂贵。在某些情况下(例如循环引用)具有共享所有权的对象可能永远不会被删除。智能指针并不是普通指针的完美替代品。
如果需要动态分配则最好将所有权保留在分配它的代码中。如果其他代码需要访问该对象请考虑向其传递副本或传递指针或引用而不转移所有权。最好使用 std::unique_ptr 来明确所有权转移。例如
std::unique_ptrFoo FooFactory();
void FooConsumer(std::unique_ptrFoo ptr);如果没有充分理由请不要将代码设计为使用共享所有权。其中一个原因是避免昂贵的复制操作但只有在性能优势显著且底层对象不可变即 std::shared_ptrconst Foo时才应这样做。如果您确实使用共享所有权则最好使用 std::shared_ptr。
切勿使用 std::auto_ptr。相反请使用 std::unique_ptr。
1.2 右值引用
仅在下面列出的某些特殊情况下使用右值引用。
右值引用是一种只能绑定到临时对象的引用。其语法与传统引用语法类似。例如void f(std::string s); 声明一个函数其参数是对 std::string 的右值引用。
当将标记“”应用于函数参数中的非限定模板参数时将应用特殊的模板参数推导规则。这种引用称为转发引用。
优点 定义移动构造函数(采用类类型的右值引用的构造函数)可以移动值而不是复制它。 例如如果 v1 是 std::vectorstd::string那么 auto v2(std::move(v1)) 可能只会导致一些简单的指针操作而不是复制大量数据。在许多情况下这可以带来显著的性能提升。 右值引用可以实现可移动但不可复制的类型这对于没有合理复制定义但您可能仍想将它们作为函数参数传递、将它们放入容器等的类型非常有用。 std::move 是有效使用某些标准库类型(如 std::unique_ptr)所必需的。 使用右值引用标记的转发引用可以编写一个通用函数包装器将其参数转发给另一个函数并且无论其参数是临时对象还是 const 都可以工作。这被称为‘完美转发’。
缺点
**右值引用尚未被广泛理解。**引用折叠和转发引用的特殊推导规则等规则有些晦涩难懂。右值引用经常被误用。在函数调用后参数应具有有效的指定状态或者不执行任何移动操作的情况下在签名中使用右值引用是违反直觉的。
不要使用右值引用或将 限定符应用于方法但以下情况除外
您可以使用它们来定义移动构造函数和移动赋值运算符(如可复制和可移动类型中所述)。您可以使用它们来定义 限定的方法这些方法在逻辑上“使用” *this使其处于不可用或空状态。请注意这仅适用于方法限定符(位于函数签名的右括号之后)如果您想“使用”普通函数参数最好按值传递它。您可以将转发引用与 std::forward 结合使用以支持完美转发。您可以使用它们来定义重载对例如一个采用 Foo另一个采用 const Foo。通常首选解决方案只是按值传递但重载函数对有时会产生更好的性能例如如果函数有时不消耗输入。一如既往如果您为了提高性能而编写更复杂的代码请确保有证据证明它确实有帮助。
1.3 友元
我们允许在合理范围内使用友元类和函数。
友元通常应在同一个文件中定义以便读者不必在另一个文件中查找类的私有成员的用途。 友元的常见用途是让 FooBuilder 类成为 Foo 的友元以便它可以正确构造 Foo 的内部状态而无需将此状态暴露给外部。 在某些情况下将 unittest 类设为其测试类的友元可能会很有用。
友元扩展了类的封装边界但不会破坏该边界。 在某些情况下当您只想让另一个类访问成员时这比将成员设为公共更好。 但是大多数类应该仅通过其公共成员与其他类交互。
1.4 异常
我们不使用 C 异常。
优点
异常允许应用程序的更高级别决定如何处理深度嵌套函数中“不可能发生的”故障而无需记录模糊且容易出错的错误代码。大多数其他现代语言都使用异常。在 C 中使用它们会使其与其他人熟悉的 Python、Java 和 C 更加一致。一些第三方 C 库使用异常在内部关闭它们会使与这些库集成变得更加困难。异常是构造函数失败的唯一方式。我们可以使用工厂函数或 Init() 方法来模拟这种情况但它们分别需要堆分配或新的“无效”状态。异常在测试框架中非常方便。
缺点
当您向现有函数添加 throw 语句时必须检查其所有传递调用者。他们必须至少提供基本的异常安全保证或者他们必须永远不捕获异常并乐于让程序终止。例如如果 f() 调用 g() 调用 h()并且 h 抛出 f 捕获的异常则 g 必须小心否则可能无法正确清理。更一般地说异常使得通过查看代码很难评估程序的控制流函数可能会在您意想不到的地方返回。这会导致可维护性和调试困难。您可以通过一些关于如何以及在何处使用异常的规则来最大限度地降低这种成本但代价是开发人员需要知道和理解更多。异常安全需要 RAII 和不同的编码实践。需要大量的支持机制才能轻松编写正确的异常安全代码。此外为了避免要求读者理解整个调用图异常安全代码必须将写入持久状态的逻辑隔离到“提交”阶段。这既有好处也有代价也许你不得不混淆代码来隔离提交。允许异常会迫使我们总是付出这些代价即使这些代价不值得。打开异常会将数据添加到生成的每个二进制文件中从而增加编译时间可能略有增加并可能增加地址空间压力。异常的可用性可能会鼓励开发人员在不合适时抛出它们或者在不安全时从中恢复。例如无效的用户输入不应导致抛出异常。
从表面上看使用异常的好处大于成本尤其是在新项目中。但是对于现有代码引入异常会对所有依赖代码产生影响。如果异常可以传播到新项目之外那么将新项目集成到现有的无异常代码中也会变得有问题。由于 Google 现有的大多数 C 代码都没有准备好处理异常因此采用会产生异常的新代码相对困难。
鉴于 Google 现有的代码不容忍异常使用异常的成本略高于新项目的成本。转换过程会很慢且容易出错。我们不认为可用的异常替代方案(例如错误代码和断言)会带来很大的负担。
我们反对使用异常的建议不是基于哲学或道德理由而是基于实践理由。因为我们想在 Google 使用我们的开源项目而如果这些项目使用异常那么这样做会很困难所以我们也需要建议不要在 Google 开源项目中使用异常。如果我们必须从头开始情况可能会有所不同。
此禁令也适用于异常处理相关功能例如 std::exception_ptr 和 std::nested_exception。
对于 Windows 代码此规则有一个例外(无意双关)。
1.5 无异常
当 noexcept 有用且正确时指定它。
noexcept 说明符用于指定函数是否会抛出异常。如果异常从标记为 noexcept 的函数中逃逸程序将通过 std::terminate 崩溃。
noexcept 运算符执行编译时检查如果表达式被声明为不抛出任何异常则返回 true。
优点
在某些情况下将移动构造函数指定为 noexcept 可以提高性能例如如果 T 的移动构造函数为 noexcept则 std::vectorT::resize() 会移动而不是复制对象。在启用异常的环境中在函数上指定 noexcept 可以触发编译器优化例如如果编译器知道由于 noexcept 说明符不会引发任何异常则它不必为堆栈展开生成额外的代码。
缺点
在遵循本指南并禁用异常的项目中很难确保 noexcept 说明符是正确的甚至很难定义正确性的含义。很难(甚至不可能)撤消 noexcept因为它以难以检测的方式消除了调用者可能依赖的保证。
如果 noexcept 能够准确反映函数的预期语义即如果函数主体内部以某种方式抛出异常则表示出现致命错误则可以使用 noexcept 来提高性能。您可以假设移动构造函数上的 noexcept 具有显著的性能优势。如果您认为在其他函数上指定 noexcept 具有显著的性能优势请与您的项目负责人讨论。
如果完全禁用异常(即大多数 Google C 环境)则首选无条件 noexcept。否则使用条件 noexcept 说明符和简单条件仅在函数可能抛出的少数情况下评估为 false。
测试可能包括类型特征检查所涉及的操作是否可能抛出(例如移动构造对象的 std::is_nothrow_move_constructible)或分配是否可以抛出例如标准默认分配的 absl::default_allocator_is_nothrow。
请注意在许多情况下异常的唯一可能原因是分配失败(我们认为移动构造函数不应抛出除非由于分配失败)并且在许多应用程序中将内存耗尽视为致命错误而不是程序应尝试恢复的异常情况是适当的。
即使对于其他潜在故障您也应该优先考虑接口简单性而不是支持所有可能的异常抛出场景例如不要编写一个依赖于哈希函数是否可以抛出的复杂 noexcept 子句只需记录您的组件不支持哈希函数抛出并使其无条件 noexcept。
1.6 运行时类型信息
避免使用运行时类型信息(RTTI)。RTTI 允许程序员在运行时查询对象的 C 类。这是通过使用 typeid 或 dynamic_cast 来实现的。
RTTI 的标准替代方案(如下所述)需要修改或重新设计相关的类层次结构。有时这种修改不可行或不受欢迎尤其是在广泛使用或成熟的代码中。
RTTI 在某些单元测试中很有用。例如它在工厂类的测试中很有用其中测试必须验证新创建的对象是否具有预期的动态类型。它在管理对象与其模拟之间的关系方面也很有用。
在考虑多个抽象对象时RTTI 很有用。考虑
bool Base::Equal(Base* other) 0;
bool Derived::Equal(Base* other) {Derived* that dynamic_castDerived*(other);if (that nullptr)return false;...
}在运行时查询对象的类型通常意味着设计问题。需要在运行时知道对象的类型通常表明类层次结构的设计存在缺陷。
不规范地使用 RTTI 会使代码难以维护。它可能导致基于类型的决策树或 switch 语句散布在整个代码中在进行进一步更改时必须检查所有这些语句。
RTTI 有合法用途但容易被滥用因此使用时必须小心谨慎。您可以在单元测试中自由使用它但应尽可能避免在其他代码中使用它。特别是在新代码中使用 RTTI 之前要三思。如果您发现自己需要编写根据对象的类而行为不同的代码请考虑以下查询类型的替代方法之一
虚拟方法是根据特定子类类型执行不同代码路径的首选方式。这会将工作放在对象本身内。如果工作属于对象之外而是属于某些处理代码请考虑使用双分派解决方案例如访问者设计模式。这允许对象本身之外的工具使用内置类型系统确定类的类型。
当程序的逻辑保证基类的给定实例实际上是特定派生类的实例时则可以在对象上自由使用 dynamic_cast。通常在这种情况下可以使用 static_cast 作为替代方案。
基于类型的决策树强烈表明您的代码走在了错误的轨道上。
if (typeid(*data) typeid(D1)) {...
} else if (typeid(*data) typeid(D2)) {...
} else if (typeid(*data) typeid(D3)) {
...当向类层次结构中添加其他子类时此类代码通常会中断。此外当子类的属性发生变化时很难找到并修改所有受影响的代码段。
不要手动实现类似 RTTI 的解决方法。反对 RTTI 的论点同样适用于带有类型标记的类层次结构等解决方法。此外解决方法会掩盖您的真实意图。
1.7 强制转换
使用 C 风格的强制类型转换例如 static_castfloat(double_value)或使用括号初始化来转换算术类型例如 int64_t y int64_t{1} 42。除非强制类型转换为 void否则不要使用 (int)x 之类的强制类型转换格式。
只有当 T 是类类型时才可以使用 T(x) 之类的强制类型转换格式。
C 引入了与 C 不同的强制类型转换系统用于区分强制类型转换运算的类型。
C 强制类型转换的问题在于操作的歧义性有时您正在进行转换(例如(int)3.5)有时您正在进行强制类型转换(例如(int)“hello”)。括号初始化和 C 强制类型转换通常可以帮助避免这种歧义。此外在搜索 C 强制类型转换时它们更明显。
C 风格的强制类型转换语法冗长而繁琐。
一般情况下不要使用 C 样式强制转换。相反当需要显式类型转换时请使用这些 C 样式强制转换。 使用括号初始化来转换算术类型(例如int64_t{x})。这是最安全的方法因为如果转换会导致信息丢失代码将无法编译。语法也很简洁。 使用 absl::implicit_cast 安全地向上转换类型层次结构例如将 Foo* 转换为 SuperclassOfFoo* 或将 Foo* 转换为 const Foo*。C 通常会自动执行此操作但在某些情况下需要显式向上转换例如使用 ?: 运算符。 当您需要显式将指针从类向上转换为其超类或者当您需要显式将指针从超类转换为子类时使用 static_cast 作为执行值转换的 C 样式强制转换的等效项。在最后一种情况下您必须确保您的对象实际上是子类的一个实例。 使用 const_cast 删除 const 限定符参见 const。 使用 reinterpret_cast 将指针类型与整数和其他指针类型包括 void*进行不安全的转换。仅当您知道自己在做什么并且了解别名问题时才使用此功能。此外请考虑取消引用指针不进行强制转换并使用 std::bit_cast 强制转换结果值。 使用 std::bit_cast 使用相同大小的不同类型类型双关语解释值的原始位例如将 double 的位解释为 int64_t。
有关使用 dynamic_cast 的指导请参阅 RTTI 部分。
1.8 IO流
在适当的情况下使用流并坚持“简单”用法。重载仅用于表示值的类型进行流式传输并且只写入用户可见的值而不写入任何实现细节。
流是 C 中的标准 I/O 抽象例如标准头文件 iostream。它们在 Google 代码中被广泛使用主要用于调试日志记录和测试诊断。
优点 和 流运算符为格式化 I/O 提供了易于学习、可移植、可重用和可扩展的 API。相比之下printf 甚至不支持 std::string更不用说用户定义类型了并且很难移植使用。printf 还迫使您在该函数的众多略有不同的版本中进行选择并浏览数十个转换说明符。 流通过 std::cin、std::cout、std::cerr 和 std::clog 为控制台 I/O 提供一流的支持。C API 也一样但由于需要手动缓冲输入而受到阻碍。
缺点
可以通过改变流的状态来配置流格式。此类改变是持久的因此您的代码的行为可能会受到流的整个先前历史的影响除非您每次在其他代码可能触碰它时都特意将其恢复到已知状态。用户代码不仅可以修改内置状态还可以通过注册系统添加新的状态变量和行为。由于上述问题、流代码中代码和数据的混合方式以及运算符重载的使用(可能选择与您预期不同的重载)很难精确控制流输出。通过 运算符链构建输出的做法会干扰国际化因为它会将字序嵌入代码中并且流对本地化的支持存在缺陷。流 API 微妙而复杂因此程序员必须积累经验才能有效地使用它。解决 的许多重载对于编译器来说成本极高。当在大型代码库中广泛使用时它会消耗多达 20% 的解析和语义分析时间。
仅当流是最适合该工作的工具时才使用流。通常当 I/O 是临时的、本地的、人类可读的并且针对其他开发人员而不是最终用户时就是这种情况。与您周围的代码以及整个代码库保持一致如果有针对您的问题的既定工具请使用该工具。特别是对于诊断输出日志记录库通常是比 std::cerr 或 std::clog 更好的选择而 absl/strings 或等效库中的库通常是比 std::stringstream 更好的选择。
避免将流用于面向外部用户或处理不受信任数据的 I/O。相反找到并使用适当的模板库来处理国际化、本地化和安全强化等问题。
如果您确实使用流请避免使用流 API 的有状态部分错误状态除外例如 imbue()、xalloc() 和 register_callback()。使用显式格式化函数例如 absl::StreamFormat()而不是流操纵器或格式化标志来控制格式化细节例如数字基数、精度或填充。
仅当您的类型表示一个值时才将 重载为您的类型的流式运算符并且 会写出该值的人性化字符串表示。避免在 的输出中公开实现细节如果您需要打印对象内部以进行调试请改用命名函数名为 DebugString() 的方法是最常见的惯例。
1.9 预增和预减
除非需要后缀语义否则请使用增量和减量运算符的前缀形式 (i)。
当变量递增(i 或 i)或递减(–i 或 i–)且表达式的值未被使用时必须决定是预递增递减还是后递增递减。
后缀递增/递减表达式会计算出修改前的值。这会导致代码更紧凑但更难阅读。前缀形式通常更易读效率也不会降低而且效率更高因为它不需要复制操作前的值。
在 C 语言中即使不使用表达式值也会使用后增这在 for 循环中尤为常见。
使用前缀增量/减少除非代码明确需要后缀增量/减少表达式的结果。
1.10 使用常量
在 API 中只要有意义就使用 const。对于某些 const 用途来说constexpr 是更好的选择。
声明的变量和参数前面可以加上关键字 const以表明变量不会改变例如const int foo。类函数可以有 const 限定符以表明函数不会改变类成员变量的状态例如class Foo { int Bar(char c) const; };。
让人们更容易理解变量的使用方式。允许编译器进行更好的类型检查并且可以生成更好的代码。帮助人们确信程序的正确性因为他们知道他们调用的函数在修改变量的方式上是有限的。帮助人们知道在多线程程序中哪些函数可以安全使用而无需锁定。
const 是病毒式的如果你将一个 const 变量传递给一个函数那么该函数的原型中必须有 const否则该变量将需要 const_cast。调用库函数时这可能是一个特殊的问题。
我们强烈建议在有意义且准确的 API即函数参数、方法和非局部变量中使用 const。这提供了一致的、大多数经过编译器验证的文档说明操作可以改变哪些对象。拥有一致且可靠的方法来区分读取和写入对于编写线程安全代码至关重要并且在许多其他情况下也很有用。特别是 如果函数保证不会修改通过引用或指针传递的参数则相应的函数参数应分别为引用到 const (const T) 或指针到 const (const T*)。 对于通过值传递的函数参数const 对调用者没有影响因此不建议在函数声明中使用。 将方法声明为 const除非它们会改变对象的逻辑状态或允许用户修改该状态例如通过返回非常量引用但这种情况很少见或者它们不能安全地同时调用。
我们既不鼓励也不反对在局部变量上使用 const。
类的所有 const 操作都应该能够安全地同时调用。如果这不可行则必须明确将该类记录为“线程不安全”。
有些人喜欢将 int const *foo 改为 const int* foo。他们认为这种形式更易读因为它更一致它遵循了 const 始终遵循其描述的对象的规则。但是这种一致性论点不适用于嵌套指针表达式较少的代码库因为大多数 const 表达式只有一个 const并且它适用于底层值。在这种情况下无需保持一致性。将 const 放在首位可以说更易读因为它遵循英语将“形容词”const放在“名词”int之前的做法。
话虽如此虽然我们鼓励将 const 放在首位但我们并不要求这样做。但要与周围的代码保持一致
1.11 常量使用
使用 constexpr 定义真正的常量或确保常量初始化。使用 constinit 确保非常量变量的常量初始化。
可以将某些变量声明为 constexpr以表明这些变量是真正的常量即在编译/链接时固定。可以将某些函数和构造函数声明为 constexpr这样它们便可用于定义 constexpr 变量。可以将函数声明为 consteval以将其使用限制在编译时。
使用 constexpr 可以用浮点表达式而不是文字来定义常量定义用户定义类型的常量以及用函数调用来定义常量。
如果过早将某个对象标记为 constexpr则在以后需要降级时可能会导致迁移问题。目前对 constexpr 函数和构造函数中允许的内容的限制可能会导致在这些定义中使用晦涩难懂的解决方法。
constexpr 定义可以更可靠地指定接口的常量部分。使用 constexpr 指定真正的常量以及支持其定义的函数。consteval 可用于运行时不得调用的代码。避免使函数定义复杂化以使其能够与 constexpr 一起使用。不要使用 constexpr 或 consteval 强制内联。
使用 constexpr 定义真常量或确保常量初始化。使用 constinit 确保非常量变量的常量初始化。
可以将某些变量声明为 constexpr以指示这些变量是真常量即在编译/链接时固定。可以将某些函数和构造函数声明为 constexpr这样它们就可以用于定义 constexpr 变量。可以将函数声明为 consteval以将其使用限制在编译时。
使用 constexpr 可以使用浮点表达式(而不仅仅是文字)来定义常量定义用户定义类型的常量以及使用函数调用来定义常量。
如果以后必须降级过早将某些内容标记为 constexpr 可能会导致迁移问题。当前对 constexpr 函数和构造函数中允许的内容的限制可能会导致在这些定义中使用模糊的解决方法。
constexpr 定义可以更可靠地指定接口的常量部分。使用 constexpr 指定真常量和支持其定义的函数。 consteval 可用于运行时不得调用的代码。避免使函数定义复杂化以使其与 constexpr 一起使用。不要使用 constexpr 或 consteval 强制内联。
1.12 整数类型
在内置的 C 整数类型中唯一使用的就是 int。如果程序需要不同大小的整数类型请使用 cstdint 中的精确宽度整数类型例如 int16_t。如果您的值可能大于或等于 2^31请使用 64 位类型例如 int64_t。请记住即使您的值对于 int 来说永远不会太大它也可能用于可能需要更大类型的中间计算。如有疑问请选择更大的类型。
C 没有为 int 等整数类型指定精确的大小。当代架构上的常见大小是 short 为 16 位int 为 32 位long 为 32 或 64 位long long 为 64 位但不同平台会做出不同的选择尤其是 long。
优点是声明的统一性。缺点是C 中整数类型的大小可能根据编译器和体系结构而变化。
标准库头文件 cstdint 定义了 int16_t、uint32_t、int64_t 等类型。当您需要保证整数的大小时您应该始终优先使用这些类型而不是 short、unsigned long long 等。最好省略这些类型的 std:: 前缀因为额外的 5 个字符不值得增加混乱。在内置整数类型中只应使用 int。在适当的情况下欢迎您使用标准类型别名如 size_t 和 ptrdiff_t。
我们经常使用 int因为我们知道整数不会太大例如循环计数器。对于这样的事情请使用普通的 int。您应该假设 int 至少为 32 位但不要假设它有超过 32 位。如果您需要 64 位整数类型请使用 int64_t 或 uint64_t。
对于我们知道可能“很大”的整数请使用 int64_t。
您不应使用无符号整数类型例如 uint32_t除非有正当理由(例如表示位模式而不是数字)或者您需要定义模 2^N 的溢出。特别是不要使用无符号类型来表示数字永远不会为负数。相反请使用断言。
如果您的代码是返回大小的容器请确保使用可以适应容器的任何可能用途的类型。如有疑问请使用较大的类型而不是较小的类型。转换整数类型时要小心。整数转换和提升可能会导致未定义的行为从而导致安全错误和其他问题。
无符号整数适合表示位域和模块化算法。由于历史原因C 标准也使用无符号整数来表示容器的大小, 标准机构的许多成员认为这是一个错误但目前实际上无法修复。无符号算法不模拟简单整数的行为而是由标准定义为模拟模块化算法(溢出/下溢时回绕)这意味着编译器无法诊断出大量错误。在其他情况下定义的行为会阻碍优化。
也就是说混合整数类型的符号性会导致同样多的问题。我们能提供的最佳建议是尝试使用迭代器和容器而不是指针和大小尽量不要混合符号性并尽量避免使用无符号类型除了表示位域或模块化算法。不要仅仅为了断言变量是非负的而使用无符号类型。
1.13 浮点数
在内置的 C 浮点类型中唯一使用的就是 float 和 double。您可以假设这些类型分别代表 IEEE-754 binary32 和 binary64。 不要使用 long double因为它会产生不可移植的结果。
1.14 架构特性
编写可移植架构的代码。不要依赖特定于单个处理器的 CPU 功能。 打印值时使用类型安全的数字格式化库如 absl::StrCat、absl::Substitute、absl::StrFormat 或 std::ostream而不是 printf 系列函数。 将结构化数据移入或移出进程时使用序列化库(如 Protocol Buffers)对其进行编码而不是复制内存中的表示形式。 如果需要将内存地址用作整数请将它们存储在 uintptr_ts 中而不是 uint32_ts 或 uint64_ts 中。 根据需要使用大括号初始化来创建 64 位常量。
int64_t my_value{0x123456789};
uint64_t my_mask{uint64_t{3} 48};使用可移植浮点类型避免使用 long double。使用可移植的整数类型避免使用 short、long 和 long long。
1.15 预处理器宏
避免定义宏尤其是在标头中最好使用内联函数、枚举和 const 变量。使用项目特定的前缀命名宏。不要使用宏来定义 C API 的各个部分。
宏意味着您看到的代码与编译器看到的代码不同。这可能会导致意外行为尤其是因为宏具有全局作用域。
当宏用于定义 C API 的各个部分时宏引入的问题尤其严重对于公共 API 更是如此。当开发人员错误地使用该接口时编译器发出的每条错误消息现在都必须解释宏是如何形成接口的。重构和分析工具在更新接口时会遇到极大的困难。因此我们特别禁止以这种方式使用宏。例如避免使用以下模式
class WOMBAT_TYPE(Foo) {// ...public:EXPAND_PUBLIC_WOMBAT_API(Foo)EXPAND_WOMBAT_COMPARISONS(Foo, , )
};幸运的是宏在 C 中的必要性远不如在 C 中那么高。
不要使用宏来内联性能关键型代码而是使用内联函数。不要使用宏来存储常量而是使用 const 变量。不要使用宏来“缩写”长变量名而是使用引用。不要使用宏来有条件地编译代码……好吧根本不要这样做(当然除了 #define 保护以防止重复包含头文件)。这会使测试变得更加困难。
宏可以做其他技术无法做到的事情而且您确实可以在代码库中看到它们尤其是在较低级别的库中。而且它们的一些特殊功能(如字符串化、连接等)无法通过语言本身获得。但在使用宏之前请仔细考虑是否有非宏方法来实现相同的结果。如果您需要使用宏来定义接口请联系您的项目负责人以请求豁免此规则。
以下使用模式将避免宏的许多问题如果您使用宏请尽可能遵循它 不要在 .h 文件中定义宏。 在使用宏之前先 #define 宏然后在使用宏之后立即 #undef 宏。 不要在用自己的宏替换现有宏之前先 #undef 宏而是选择一个可能唯一的名称。 尽量不要使用会扩展为不平衡 C 构造的宏或者至少要很好地记录该行为。 最好不要使用 ## 来生成函数/类/变量名称。
强烈建议不要从标头导出宏(即在标头中定义宏而不在标头末尾之前 #undef 宏)。如果您确实从标头导出宏则它必须具有全局唯一的名称。为此它必须使用由项目命名空间名称(但大写)组成的前缀命名。
1.16 0和nullptr(NULL)
对指针使用 nullptr对字符使用 \0(而不是 0 文字)对于指针(地址值)请使用 nullptr因为这可提供类型安全性。
对空字符使用 \0。使用正确的类型可使代码更具可读性。
1.17 sizeof获取大小
优先使用 sizeof(varname) 而不是 sizeof(type)。
获取特定变量的大小时使用 sizeof(varname)。如果有人现在或以后更改变量类型sizeof(varname) 将进行相应更新。您可以将 sizeof(type) 用于与任何特定变量无关的代码例如管理外部或内部数据格式的代码其中适当的 C 类型的变量不方便。
// 推荐代码
MyStruct data;
memset(data, 0, sizeof(data));// 不推荐代码
memset(data, 0, sizeof(MyStruct));// 推荐代码
if (raw_size sizeof(int)) {LOG(ERROR) 压缩记录不够大无法计数 raw_size;return false;
}1.18 类型推断(包括自动)
仅当类型推断可以让不熟悉项目的读者更清楚地理解代码或者让代码更安全时才使用类型推断。不要仅仅为了避免编写显式类型的不便而使用它。
在以下几种情况下C 允许(甚至要求)编译器推断类型而不是在代码中明确说明 函数模板参数推导函数模板可以在没有显式模板参数的情况下调用。编译器根据函数参数的类型推导这些参数 template typename T
void f(T t);f(0); // Invokes fint(0)自动变量声明变量声明可以使用 auto 关键字代替类型。编译器根据变量的初始化器推断出类型遵循与使用相同初始化器推断函数模板参数相同的规则(只要您不使用花括号代替圆括号)。 auto a 42; // a is an int
auto b a; // b is an int
auto c b; // c is an int
auto d{42}; // d is an int, not a std::initializer_listintauto 可以用 const 限定并且可以用作指针或引用类型的一部分以及(自 C17 起)用作非类型模板参数。此语法的罕见变体使用 decltype(auto) 而不是 auto在这种情况下推导的类型是将 decltype 应用于初始化程序的结果。 函数返回类型推导auto(和 decltype(auto))也可用于代替函数返回类型。编译器根据函数主体中的返回语句推断返回类型遵循与变量声明相同的规则 auto f() { return 0; } // The return type of f is intLambda 表达式的返回类型可以用相同的方式推断但这是通过省略返回类型而不是通过显式 auto 来触发的。令人困惑的是函数的尾随返回类型语法也在返回类型位置使用 auto但这并不依赖于类型推断它只是显式返回类型的替代语法。 通用 lambdaLambda 表达式可以使用 auto 关键字代替其一个或多个参数类型。这会导致 lambda 的调用运算符成为函数模板而不是普通函数每个自动函数参数都有一个单独的模板参数 // 按降序对 vec 进行排序
std::sort(vec.begin(), vec.end(), { return lhs rhs; });Lambda init 捕获Lambda 捕获可以有显式的初始化器可以用来声明全新的变量而不是仅仅捕获现有的变量 [x 42, y foo] { ... } // x is an int, and y is a const char*此语法不允许指定类型而是使用自动变量的规则来推断类型。 结构化绑定使用 auto 声明元组、结构或数组时您可以指定各个元素的名称而不是整个对象的名称这些名称称为“结构化绑定”整个声明称为“结构化绑定声明”。此语法无法指定封闭对象或各个名称的类型 auto [iter, success] my_map.insert({key, value});
if (!success) {iter-second value;
}auto 也可以用 const、 和 限定但请注意这些限定符在技术上适用于匿名元组/结构/数组而不是单个绑定。确定绑定类型的规则非常复杂结果往往并不令人惊讶只是即使声明了引用绑定类型通常也不会是引用但它们通常仍然表现得像引用。
优点: C 类型名称可能很长且繁琐尤其是当它们涉及模板或命名空间时。 当 C 类型名称在单个声明或小代码区域内重复时重复可能无助于提高可读性。 有时让类型推断出来更安全因为这样可以避免意外复制或类型转换的可能性。
缺点
当类型明确时C 代码通常更清晰尤其是当类型推断依赖于来自代码远处的信息时。在以下表达式中
auto foo x.add_foo();
auto i y.Find(key);如果 y 的类型不太为人所知或者 y 声明于很多行之前则结果类型可能不明显。
程序员必须了解类型推导何时会产生或不会产生引用类型否则他们会在无意的情况下获得副本。
如果推导类型用作接口的一部分那么程序员可能会更改其类型而只是想更改其值从而导致比预期更彻底的 API 更改。
基本规则是仅使用类型推导来使代码更清晰或更安全不要仅仅为了避免编写显式类型的不便而使用它。在判断代码是否更清晰时请记住您的读者不一定在您的团队中也不一定是熟悉您的项目的人因此您和您的审阅者认为不必要的混乱类型通常会为其他人提供有用的信息。例如您可以假设 make_uniqueFoo() 的返回类型是显而易见的但 MyWidgetFactory() 的返回类型可能不是。
这些原则适用于所有形式的类型推导但细节会有所不同如以下部分所述。 函数模板实参推导函数模板参数推导几乎总是可以的。类型推导是与函数模板交互的预期默认方式因为它允许函数模板像无限的普通函数重载集一样工作。因此函数模板几乎总是设计为模板参数推导清晰且安全否则不会编译。 局部变量类型推断对于局部变量可以使用类型推断来消除明显或不相关的类型信息从而使代码更清晰以便读者可以专注于代码中有意义的部分 std::unique_ptrWidgetWithBellsAndWhistles widget std::make_uniqueWidgetWithBellsAndWhistles(arg1, arg2);
absl::flat_hash_mapstd::string,std::unique_ptrWidgetWithBellsAndWhistles::const_iteratorit my_map_.find(key);
std::arrayint, 6 numbers {4, 8, 15, 16, 23, 42};auto widget std::make_uniqueWidgetWithBellsAndWhistles(arg1, arg2);
auto it my_map_.find(key);
std::array numbers {4, 8, 15, 16, 23, 42};类型有时包含有用信息和样板例如上例中的情况很明显该类型是一个迭代器并且在许多上下文中容器类型甚至键类型都不相关但值的类型可能很有用。在这种情况下通常可以定义具有明确类型的局部变量来传达相关信息 if (auto it my_map_.find(key); it ! my_map_.end()) {WidgetWithBellsAndWhistles widget *it-second;// Do stuff with widget
}如果类型是模板实例并且参数是样板但模板本身是有用的则可以使用类模板参数推导来抑制样板。但是这实际上提供有意义的好处的情况非常罕见。请注意类模板参数推导也受单独的样式规则约束。 如果更简单的选项可行请不要使用 decltype(auto)因为它是一个相当晦涩的功能因此在代码清晰度方面成本很高。 返回类型推导仅当函数主体的返回语句数量很少且其他代码很少时才使用返回类型推导(对于函数和 lambda 来说)因为否则读者可能无法一眼看出返回类型是什么。此外仅当函数或 lambda 的范围非常窄时才使用它因为具有推导返回类型的函数不定义抽象边界实现就是接口。特别是头文件中的公共函数几乎永远不应该具有推导返回类型。 参数类型推断应谨慎使用 lambda 的自动参数类型因为实际类型由调用 lambda 的代码决定而不是由 lambda 的定义决定。因此显式类型几乎总是更清晰除非在非常接近定义的位置显式调用 lambda(以便读者可以轻松看到两者)或者将 lambda 传递给一个众所周知的接口以至于很明显它最终将使用哪些参数来调用它(例如上面的 std::sort 示例)。 Lambda 初始化捕获初始化捕获由更具体的样式规则涵盖该规则在很大程度上取代了类型推断的一般规则。 结构化绑定与其他类型的类型推导不同结构化绑定实际上可以通过为较大对象的元素赋予有意义的名称来为读者提供更多信息。这意味着结构化绑定声明可能比显式类型提供净可读性改进即使在 auto 不会这样做的情况下也是如此。当对象是一对或元组如上面的 insert 示例时结构化绑定尤其有用因为它们一开始就没有有意义的字段名称但请注意除非预先存在的 API如 insert强迫您使用否则通常不应使用对或元组。 如果绑定的对象是结构体提供更适合您用途的名称有时可能会有所帮助但请记住这也可能意味着这些名称对读者来说比字段名称更难识别。我们建议使用注释来指示底层字段的名称(如果它与绑定的名称不匹配)使用与函数参数注释相同的语法 auto [/*field_name1*/bound_name1, /*field_name2*/bound_name2] ...与函数参数注释一样这可以使工具检测出字段的顺序是否错误。
1.19 类模板参数推导
仅对明确选择支持类模板参数推导的模板使用类模板参数推导。
类模板参数推导(通常缩写为“CTAD”)发生在当使用命名模板的类型声明变量并且未提供模板参数列表甚至没有空尖括号时
std::array a {1, 2, 3}; // a is a std::arrayint, 3编译器使用模板的“推导指南”从初始化程序中推导参数这些指南可以是显式的也可以是隐式的。
显式推导指南看起来像带有尾随返回类型的函数声明只是没有前导 auto并且函数名称是模板的名称。
例如上面的示例依赖于 std::array 的此推导指南
namespace std {
template class T, class... U
array(T, U...) - std::arrayT, 1 sizeof...(U);
}主模板中的构造函数(与模板特化相反)也隐式定义推导指南。
当您声明依赖于 CTAD 的变量时编译器会使用构造函数重载解析规则选择推导指南并且该指南的返回类型将成为变量的类型。
CTAD 有时可以让您从代码中省略样板。
从构造函数生成的隐式推导指南可能具有不良行为或完全不正确。这对于在 C17 中引入 CTAD 之前编写的构造函数尤其成问题因为这些构造函数的作者无法知道(更不用说修复)他们的构造函数会给 CTAD 带来的任何问题。此外添加显式推导指南来修复这些问题可能会破坏任何依赖隐式推导指南的现有代码。
CTAD 也存在与 auto 相同的许多缺点因为它们都是从变量的初始化程序中推导变量类型全部或部分的机制。 CTAD 确实为读者提供了比 auto 更多的信息但它也没有给读者一个明显的提示表明信息已被省略。
不要将 CTAD 与给定模板一起使用除非模板维护者已选择通过提供至少一个显式推导指南来支持使用 CTAD(std 命名空间中的所有模板也假定已选择加入)。如果可用应使用编译器警告强制执行此操作。
CTAD 的使用还必须遵循类型推导的一般规则。
1.20 指定初始化器
仅以符合 C20 的形式使用指定的初始化器。
指定初始化器是一种语法允许通过明确命名其字段来初始化聚合“普通旧结构” struct Point {float x 0.0;float y 0.0;float z 0.0;};Point p {.x 1.0,.y 2.0,// z will be 0.0};明确列出的字段将按指定方式初始化其他字段将以与传统聚合初始化表达式如 Point{1.0, 2.0}相同的方式初始化。
指定初始化器可以生成方便且高度可读的聚合表达式特别是对于字段顺序不如上面的 Point 示例那么直接的结构体。
虽然指定初始化器早已成为 C 标准的一部分并作为扩展得到 C 编译器的支持但在 C20 之前C 并不支持它们。
C 标准中的规则比 C 和编译器扩展中的规则更严格要求指定初始化器出现的顺序与结构定义中字段出现的顺序相同。因此在上面的例子中根据 C20先初始化 x 然后初始化 z 是合法的但先初始化 y 然后初始化 x 是不合法的。
仅以与 C20 标准兼容的形式使用指定初始化器初始化器的顺序与相应字段在结构定义中出现的顺序相同。
1.21 Lambda 表达式
适当时使用 lambda 表达式。当 lambda 超出当前范围时最好使用显式捕获。
Lambda 表达式是创建匿名函数对象的简洁方法。在将函数作为参数传递时它们通常很有用。例如
std::sort(v.begin(), v.end(), [](int x, int y) {return Weight(x) Weight(y);
});它们还允许通过名称显式或使用默认捕获隐式地从封闭范围捕获变量。显式捕获要求列出每个变量作为值或引用捕获
int weight 3;
int sum 0;
// Captures weight by value and sum by reference.
std::for_each(v.begin(), v.end(), [weight, sum](int x) {sum weight * x;
});默认捕获隐式捕获 lambda 主体中引用的任何变量如果使用任何成员则包括此变量
const std::vectorint lookup_table ...;
std::vectorint indices ...;
// Captures lookup_table by reference, sorts indices by the value
// of the associated element in lookup_table.
std::sort(indices.begin(), indices.end(), [](int a, int b) {return lookup_table[a] lookup_table[b];
});变量捕获还可以具有显式初始化器其可用于按值捕获仅移动变量或用于普通引用或值捕获无法处理的其他情况
std::unique_ptrFoo foo ...;
[foo std::move(foo)] () {...
}这样的捕获通常称为“初始化捕获”或“广义 lambda 捕获”实际上不需要从封闭范围“捕获”任何东西甚至不需要从封闭范围中获取名称这种语法是定义 lambda 对象成员的完全通用的方法
[foo std::vectorint({1, 2, 3})] () {...
}带有初始化器的捕获的类型使用与自动相同的规则推断。
优点 Lambdas 比其他定义要传递给 STL 算法的函数对象的方式更简洁这可以提高可读性。 适当使用默认捕获可以消除冗余并突出显示默认的重要异常。 Lambdas、std::function 和 std::bind 可以组合使用作为通用回调机制它们使编写以绑定函数为参数的函数变得容易。
缺点
lambda 中的变量捕获可能是悬空指针错误的来源特别是当 lambda 超出当前范围时。默认按值捕获可能会产生误导因为它们不能防止悬空指针错误。按值捕获指针不会导致深层复制因此它通常具有与按引用捕获相同的生命周期问题。当按值捕获 this 时这尤其令人困惑因为 this 的使用通常是隐式的。捕获实际上声明了新变量无论捕获是否具有初始化程序但它们看起来与 C 中的任何其他变量声明语法都不一样。特别是没有变量类型的位置甚至没有 auto 占位符尽管 init 捕获可以间接指示它例如使用强制类型转换。这可能使它们甚至很难被识别为声明。init 捕获本质上依赖于类型推导并且具有与 auto 相同的许多缺点还有一个问题是语法甚至没有提示读者正在进行推导。lambda 的使用可能会失控非常长的嵌套匿名函数会使代码更难理解。
在适当的情况下使用 lambda 表达式格式如下所述。
如果 lambda 可能超出当前范围则优先使用显式捕获。例如而不是
{Foo foo;...executor-Schedule([] { Frobnicate(foo); })...
}
// BAD! The fact that the lambda makes use of a reference to foo and
// possibly this (if Frobnicate is a member function) may not be
// apparent on a cursory inspection. If the lambda is invoked after
// the function returns, that would be bad, because both foo
// and the enclosing object could have been destroyed.而是下面这种形式:
{Foo foo;...executor-Schedule([foo] { Frobnicate(foo); })...
}
// BETTER - The compile will fail if Frobnicate is a member
// function, and its clearer that foo is dangerously captured by
// reference.仅当 lambda 的生命周期明显短于任何潜在捕获时才使用默认的引用捕获 ([])。仅将默认的值捕获 ([]) 用作绑定短 lambda 的几个变量的一种方式其中捕获的变量集一目了然并且不会导致隐式捕获 this。这意味着出现在非静态类成员函数中并在其主体中引用非静态类成员的 lambda 必须显式捕获 this 或通过 []。最好不要编写带有默认值捕获的长或复杂 lambda。仅使用捕获来实际捕获封闭范围中的变量。不要将捕获与初始化程序一起使用来引入新名称或大幅更改现有名称的含义。相反以常规方式声明一个新变量然后捕获它或者避免使用 lambda 简写并显式定义一个函数对象。有关指定参数和返回类型的指导请参阅类型推导部分。
1.22 模板元编程
避免复杂的模板编程。
模板元编程是指利用 C 模板实例化机制是图灵完备的这一事实的一系列技术可用于在类型域中执行任意编译时计算。
模板元编程允许极其灵活的接口这些接口类型安全且性能高。没有它GoogleTest、std::tuple、std::function 和 Boost.Spirit 等设施就不可能实现。
模板元编程中使用的技术通常对语言专家以外的任何人都晦涩难懂。以复杂方式使用模板的代码通常难以阅读并且难以调试或维护。
模板元编程通常会导致极差的编译时错误消息即使接口很简单当用户做错事时复杂的实现细节就会变得明显。
模板元编程使重构工具的工作变得更加困难从而干扰大规模重构。首先模板代码在多个上下文中展开很难验证转换在所有上下文中是否有意义。其次一些重构工具使用的 AST 仅表示模板扩展后的代码结构。很难自动返回到需要重写的原始源构造。
模板元编程有时会提供比没有它时更干净、更易于使用的接口但它也常常会让人过于聪明。它最好用于少量的低级组件在这些组件中额外的维护负担会分散到大量使用中。
在使用模板元编程或其他复杂的模板技术之前请三思想想在您切换到另一个项目后您的团队中的普通成员是否能够很好地理解您的代码以进行维护或者非 C 程序员或随意浏览代码库的人是否能够理解错误消息或跟踪他们想要调用的函数的流程。如果您使用递归模板实例化或类型列表或元函数或表达式模板或者依赖 SFINAE 或 sizeof 技巧来检测函数重载解析那么很有可能您做得太过了。
如果您使用模板元编程您应该期望投入大量精力来最小化和隔离复杂性。您应该尽可能将元编程隐藏为实现细节以便面向用户的标头可读并且您应该确保棘手的代码得到特别好的注释。您应该仔细记录代码的使用方式并且应该说明“生成的”代码是什么样子。特别注意编译器在用户犯错时发出的错误消息。错误消息是用户界面的一部分您的代码应该根据需要进行调整以便从用户的角度来看错误消息是可理解和可操作的。
1.23 概念和约束
谨慎使用概念。一般来说概念和约束只应在 C20 之前使用模板的情况下使用。避免在标头中引入新概念除非标头被标记为库内部。不要定义编译器不强制执行的概念。优先使用约束而不是模板元编程并避免使用 templateConcept T 语法而是使用 require(ConceptT) 语法。
concept 关键字是一种定义模板参数要求(例如类型特征或接口规范)的新机制。requires 关键字提供了在模板上放置匿名约束并在编译时验证约束是否得到满足的机制。概念和约束通常一起使用但也可以单独使用。
优点
概念允许编译器在涉及模板时生成更好的错误消息从而减少混乱并显著改善开发体验。概念可以减少定义和使用编译时约束所需的样板通常可以提高生成代码的清晰度。约束提供了一些难以通过模板和 SFINAE 技术实现的功能。
缺点
与模板一样概念会使代码变得更加复杂和难以理解。概念语法可能会让读者感到困惑因为概念在使用时看起来与类类型相似。概念尤其是在 API 边界会增加代码耦合、僵化和僵化。概念和约束可以从函数体中复制逻辑导致代码重复并增加维护成本。概念混淆了其底层契约的真相来源因为它们是独立的命名实体可以在多个位置使用所有这些位置都是彼此独立发展的。这可能会导致明示和暗示的要求随着时间的推移而出现分歧。概念和约束以新颖且不明显的方式影响过载解析。与 SFINAE 一样约束使大规模重构代码变得更加困难。
当存在等效概念时应优先使用标准库中的预定义概念而不是类型特征。例如如果在 C20 之前使用了 std::is_integral_v则应在 C20 代码中使用 std::integral。同样优先使用现代约束语法通过 require(Condition)。
避免使用旧模板元编程构造例如 std::enable_ifCondition以及 templateConcept T 语法。
不要手动重新实现任何现有概念或特征。例如使用 require(std::default_initializableT) 而不是 require(requires { T v; }) 或类似方法。
新概念声明应该很少见并且只在库内部定义这样它们就不会在 API 边界上暴露。更一般地说如果您不会在 C17 中使用它们的旧模板等效项请不要使用概念或约束。
不要定义重复函数体的概念也不要强加无关紧要的要求或者从阅读代码主体或产生的错误消息中显而易见的要求。例如避免以下情况
template typename T // Bad - redundant with negligible benefit
concept Addable std::copyableT requires(T a, T b) { a b; };
template Addable T
T Add(T x, T y, T z) { return x y z; }相反最好将代码保留为普通模板除非您可以证明概念可以显著改善特定情况例如对于深度嵌套或不明显的要求产生的错误消息。 概念应该由编译器静态验证。不要使用主要优点来自语义或其他未强制执行的约束的任何概念。编译时未强制执行的要求应通过其他机制例如注释、断言或测试来实施。