Other C++ Features 其他 C++ 特性
右值引用
仅在下文列出的特定特殊情况下使用右值引用。
定义:
右值引用是一种只能绑定到临时对象的引用类型。其语法与传统的引用语法相似。例如,void f(std::string&& s); 声明了一个函数,其参数是对 std::string 的右值引用。
当符号 && 作用于函数参数中的非限定模板参数时,会应用特殊的模板实参推导规则,这种引用被称为转发引用(forwarding reference)。
优点:
定义一个移动构造函数(即接受类类型右值引用的构造函数),使得移动一个值成为可能,从而避免了复制。例如,如果 v1 是一个 std::vector<std::string>,那么 auto v2(std::move(v1)) 很可能仅仅涉及一些简单的指针操作,而非复制大量的数据。在许多情况下,这能带来显著的性能提升。
右值引用使得实现可移动但不可复制的类型成为可能。这对于那些无法定义合理“复制”行为,但仍希望将其作为函数参数传递或放入容器等场景的类型非常有用。
为了有效地使用某些标准库类型(例如 std::unique_ptr),std::move 是必不可少的。
使用右值引用标记的转发引用(Forwarding References),使得编写能够将其参数转发给另一函数的通用函数包装器成为可能。无论其参数是否为临时对象或是否为 const,该包装器都能正常工作,这就是所谓的完美转发(perfect forwarding)。
缺点:
右值引用尚未被广泛透彻地理解。像引用折叠以及针对转发引用的特殊推导规则等概念,对于许多人来说仍有些晦涩难懂。
右值引用经常被误用。在以下场景中使用右值引用是违反直觉的:函数调用结束后,期望参数仍处于某种有效且明确的状态;或者函数内部根本没有执行移动操作。
结论:
你可以使用它们来定义移动构造函数和移动赋值运算符(如《可复制和可移动类型》中所述)。
你可以使用它们来定义带有 && 限定符的方法,这些方法在逻辑上会“消耗” *this,使其进入不可用或空的状态。请注意,这仅适用于方法限定符(位于函数签名右括号之后);如果你想要“消耗”一个普通的函数参数,最好还是按值传递。
你可以结合 std::forward 使用转发引用,以支持完美转发。
你可以使用它们来定义成对的重载函数,例如一个接受 Foo&&,另一个接受 const Foo&。通常首选的解决方案是按值传递,但有时成对的重载函数能产生更好的性能,例如当函数有时并不消耗输入时。与往常一样:如果你为了性能而编写更复杂的代码,请务必确保有证据表明它确实有效。
友元
在合理的情况下,我们允许使用友元类和友元函数。
友元通常应该定义在同一个文件中,这样读者就不必去其他文件查找对某个类私有成员的使用情况。一个常见的用法是让 FooBuilder 类成为 Foo 类的友元,以便它可以正确地构建 Foo 的内部状态,同时又不会将此状态暴露给外界。在某些情况下,让单元测试类成为它所测试类的友元也是有用的。
友元扩展了类的封装边界,但并没有打破它。当你只想让某一个特定类访问某个成员时,在某些情况下这比将该成员设为 public(公有)要好。然而,大多数类应该仅通过其公有成员与其他类进行交互。
异常
我们不使用 C++ 异常。
优点:
异常机制允许应用程序的高层模块来决定如何处理在深层嵌套函数中发生的“理论上不可能发生”的故障,从而避免了使用错误码带来的逻辑晦涩和出错风险。
异常被绝大多数其他现代编程语言所采用。在 C++ 中使用异常可以使代码风格与 Python、Java 以及其他开发者熟悉的 C++ 风格保持一致。
一些第三方 C++ 库使用了异常,如果在内部禁用异常,将使得与这些库的集成变得更加困难。
异常是构造函数报告失败的唯一途径。虽然我们可以使用工厂函数或 Init() 方法来模拟这一行为,但这分别需要堆内存分配,或者引入一个新的“无效”状态。
异常在测试框架中非常方便实用
缺点:
当你向一个已存在的函数中添加 throw 语句时,你必须检查其所有传递调用者(transitive callers)。这些调用者要么必须至少提供基本异常安全保证(basic exception safety guarantee),要么必须永远不捕获该异常,并乐于接受程序因此终止的结果。例如,如果 f() 调用 g(),g() 调用 h(),而 h 抛出的异常被 f 捕获,那么 g 就必须非常小心,否则可能会导致资源无法正确清理。
更普遍地说,异常使得仅通过查看代码来评估程序的控制流变得困难:函数可能会在你意想不到的地方返回。这给代码的维护和调试带来了困难。虽然你可以通过制定一些关于如何及在何处使用异常的规则来最小化这种成本,但这又增加了开发者需要学习和理解的知识量。
实现异常安全需要同时依赖 RAII(资源获取即初始化)和不同的编码习惯。为了方便编写正确的异常安全代码,需要大量的辅助机制。此外,为了避免强制读者理解整个调用图,异常安全的代码必须将写入持久状态的逻辑隔离到一个“提交”阶段。这既有好处也有成本(也许你会被迫混淆代码以隔离提交逻辑)。允许使用异常意味着即使在不值得的情况下,我们也必须一直承担这些成本。
开启异常支持会向生成的每个二进制文件中添加数据,从而增加编译时间(可能只是轻微增加),并可能增加地址空间的压力。
异常的存在可能会鼓励开发者在不恰当的时候抛出异常,或者在不安全的时候从异常中恢复。例如,无效的用户输入不应该导致抛出异常。为了记录这些限制,我们需要把编码规范写得更长!
结论:
从表面上看,使用异常的好处似乎超过了成本,尤其是在新项目中。然而,对于现有代码而言,引入异常会对所有相关依赖代码产生影响。如果异常能够传播到新项目之外,那么将其集成到现有的无异常代码中就会变得很麻烦。由于 Google 现有的大多数 C++ 代码并未准备好处理异常,因此引入会产生异常的新代码将变得相当困难。
鉴于 Google 现有的代码并不兼容异常,使用异常的成本比在新项目中要高得多。代码改造过程将缓慢且容易出错。我们认为,异常的替代方案(如错误码和断言)并不会带来显著的负担。
我们建议不要使用异常,并非基于哲学或道德层面的考量,而是出于实际工程实践的考虑。因为我们希望能在 Google 内部使用我们的开源项目,而如果这些项目使用了异常,这件事就会变得很难办。因此,我们也需要建议在 Google 的开源项目中避免使用异常。如果我们能从头再来,情况可能会有所不同。
此禁令同样适用于与异常处理相关的特性,例如 std::exception_ptr 和 std::nested_exception。
关于 Windows 代码,此规则有一个例外情况(绝非双关语)。(这里参考文档最后关于Windows代码的表述)
不抛出异常
在既有益处又符合逻辑的情况下,指定 noexcept。
noexcept 是 C++11 引入的一个关键字,它是一个说明符(Specifier)或操作符(Operator)。
当用在函数声明后面时(例如 void func() noexcept;),它向编译器承诺:这个函数绝对不会抛出异常。
定义:
noexcept 说明符用于声明一个函数是否会抛出异常。如果从被标记为 noexcept 的函数中逃逸出异常,程序将通过调用 std::terminate 而崩溃。
noexcept 操作符则执行一个编译期检查,如果一个表达式被声明为不抛出任何异常,则返回 true。
优点:
将移动构造函数指定为 noexcept 可以在某些情况下提高性能。例如,如果类型 T 的移动构造函数是 noexcept 的,那么 std::vector<T>::resize() 就会移动对象而不是复制对象。
在启用了异常的环境中,为函数指定 noexcept 可以触发编译器优化。例如,如果编译器知道由于 noexcept 说明符而不会抛出异常,那么它就不必为栈展开(stack-unwinding)生成额外的代码。
缺点:
在遵循本指南且禁用了异常的项目中,很难确保 noexcept 说明符的正确性,甚至很难定义“正确性”本身究竟意味着什么。
撤销 noexcept 说明符也是一件难事,甚至不可能做到,因为它消除了调用者可能依赖的保证,而这种依赖关系往往很难被检测到
结论:
当 noexcept 能够准确反映函数的预期语义(即如果函数体内以某种方式抛出了异常,则代表这是一个致命错误),并且能带来性能提升时,你可以使用它。你可以假定为移动构造函数指定 noexcept 具有显著的性能优势。如果你认为为其他某些函数指定 noexcept 也能带来显著的性能提升,请务必与你的项目负责人讨论。
如果异常被完全禁用(即大多数 Google C++ 环境),请优先使用无条件的 noexcept。否则,请使用带有简单条件的条件 noexcept 说明符,且仅在极少数函数可能抛出异常的情况下,让这些条件计算为 false。
这些测试可能包括检查相关操作是否可能抛出异常的类型特征(例如,使用 std::is_nothrow_move_constructible 来移动构造对象),或者检查分配是否可能抛出异常(例如,使用 absl::default_allocator_is_nothrow 进行标准默认分配)。
请注意,在许多情况下,异常的唯一可能原因是分配失败(我们认为移动构造函数不应抛出异常,除非是由于分配失败)。在许多应用程序中,将内存耗尽视为致命错误(而不是程序应尝试恢复的异常情况)是合适的。
即使对于其他潜在的失败,你也应该优先考虑接口的简洁性,而不是支持所有可能的异常抛出场景:例如,与其编写一个依赖于哈希函数是否可能抛出异常的复杂 noexcept 子句,不如简单地在文档中注明你的组件不支持会抛出异常的哈希函数,并将其声明为无条件的 noexcept。
运行时类型信息 (RTTI)
避免使用运行时类型信息 (RTTI)。
定义:
运行时类型信息(RTTI)允许程序员在程序运行时查询 C++ 对象的类。这通过使用 typeid 或 dynamic_cast 来实现。
优点:
标准的 RTTI 替代方案(如下所述)需要修改或重新设计相关的类层次结构。有时这种修改是不可行或不希望的,特别是在广泛使用或成熟的代码中。
RTTI 在某些单元测试中非常有用。例如,在测试工厂类时,测试需要验证新创建的对象是否具有预期的动态类型,此时 RTTI 就派上了用场。它在管理对象与其模拟对象(mocks)之间的关系时也很有用。
当需要处理多个抽象对象时,RTTI 非常有用。请考虑以下情况:
bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other)
{
Derived* that = dynamic_cast<Derived*>(other);
if (that == nullptr)
return false;
...
}缺点:
在运行时查询对象的类型通常意味着设计存在问题。如果需要在运行时知道对象的类型,这往往是你的类层次结构设计有缺陷的信号。
滥用 RTTI 会使代码难以维护。它可能导致基于类型的决策树或 switch 语句分散在代码各处,而在进行进一步修改时,所有这些地方都必须被检查。
结论:
RTTI 虽然有其合法的用途,但很容易被滥用,因此在使用时必须格外小心。你可以在单元测试中自由使用它,但在其他代码中应尽可能避免。特别是,在新代码中使用 RTTI 之前,请三思。如果你发现自己需要编写根据对象类而表现不同的代码,请考虑以下替代查询类型的方案之一:
当需要根据特定的子类类型执行不同的代码路径时,虚函数是首选方式。这将处理逻辑封装在对象内部。
如果该工作属于对象外部的处理代码,可以考虑使用双分派解决方案,例如 Visitor(访问者)设计模式。这允许对象外部的设施利用内置的类型系统来确定类的类型。
当程序的逻辑能够保证某个基类实例实际上是一个特定派生类的实例时,可以自由地对该对象使用 dynamic_cast。在这种情况下,通常也可以使用 static_cast 作为替代方案。
基于类型的决策树是代码设计偏离正轨的强烈信号。
if (typeid(*data) == typeid(D1))
{
...
}
else if (typeid(*data) == typeid(D2))
{
...
}
else if (typeid(*data) == typeid(D3))
{
...当向类层次结构中添加新的子类时,诸如此类的代码通常会失效。此外,当子类的属性发生变化时,很难找到并修改所有受影响的代码段。
不要手动实现类似 RTTI 的变通方案。 针对 RTTI 的反对观点同样适用于带有类型标签的类层次结构这类变通方案。此外,这些变通方案会掩盖你的真实意图。
转换 / 强制类型转换 (Casting)
请使用 C++ 风格的强制转换,例如 static_cast<float>(double_value),或者使用花括号初始化来转换算术类型,例如 int64_t y = int64_t{1} << 42。除非是要转换为 void,否则不要使用 (int)x 这样的转换格式。只有当 T 是一个类类型时,你才可以使用 T(x) 这样的转换格式。
定义:
C++ 引入了一套不同于 C 语言的强制转换系统,它能够区分强制转换操作的类型。
优点:
C 风格强制转换的主要问题在于操作的歧义性;有时你是在进行类型转换(例如,(int)3.5),而有时你是在进行强制类型转换(例如,(int)"hello")。花括号初始化(Brace initialization)和 C++ 风格的强制转换通常有助于避免这种歧义。此外,当需要在代码中搜索时,C++ 风格的强制转换也更加显眼。
缺点:
C++ 风格的强制转换语法显得冗长且麻烦。
结论:
一般来说,不要使用 C 风格的强制转换。相反,在需要显式类型转换时,请使用这些 C++ 风格的强制转换。
算术类型转换:使用花括号初始化(例如 int64_t{x})。这是最安全的方法,因为如果转换可能导致数据丢失,代码将无法通过编译,而且语法也很简洁。
转换为类类型:使用函数式风格的转换(例如,优先使用 std::string(some_cord) 而不是 static_cast<std::string>(some_cord))。
向上转型:使用 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。
请参阅 RTTI 章节,以获取关于使用 dynamic_cast 的指导建议。(上面一节内容)
输入/输出流
在合适的地方使用流,并且坚持“简单”的用法。仅针对表示值的类型重载 << 进行流式输出,并且只写入用户可见的值,而不要包含任何实现细节。
定义:
流(Streams)是 C++ 中标准的 I/O 抽象,标准头文件 <iostream> 就是其典型示例。它们在 Google 的代码库中被广泛使用,主要用于调试日志记录和测试诊断。
优点:
<< 和 >> 流运算符提供了一套用于格式化输入/输出(I/O)的 API。这套 API 具有易于学习、可移植、可重用和可扩展的优点。相比之下,printf 甚至连 std::string 都不支持,更不用说用户自定义类型了,并且很难实现可移植的使用。此外,printf 还强迫你在众多略有不同的函数版本中进行选择,并且需要弄清楚数十种转换说明符的用法。
流通过 std::cin、std::cout、std::cerr 和 std::clog 为控制台 I/O 提供了原生支持(一流支持)。C 语言的 API 虽然也提供了支持,但因为需要手动缓冲输入而显得笨拙不便。
缺点:
状态可变且持久:流的格式化行为可以通过修改其内部状态来配置。这种修改是持久性的,这意味着你的代码行为可能会受到流之前所有操作历史的影响。除非你每次都在其他代码可能触及流之后,特意将其恢复到已知状态,否则很难保证行为的一致性。用户代码不仅能修改内建的状态,还能通过注册系统添加新的状态变量和行为。
输出控制困难:由于上述状态问题、代码与数据在流式代码中混合在一起,以及运算符重载的使用(可能会选择出乎你意料的重载版本),很难精确地控制流的输出。
不利于国际化:通过一连串 << 操作符构建输出的方式会将词序硬编码到代码中,这严重干扰了国际化支持。此外,流对于本地化的支持本身也存在缺陷。
API 复杂:流的 API 设计微妙且复杂,程序员必须积累足够的使用经验才能有效地运用它。
编译性能开销:解析 << 的众多重载版本对编译器来说成本极高。如果在大型代码库中广泛使用,它可能消耗多达 20% 的语法解析和语义分析时间。
结论:
只有在流是处理特定任务的最佳工具时才使用它们。这种情况通常出现在 I/O 操作是临时的、局部的、人类可读的,并且面向其他开发人员而非最终用户时。请与周围的代码以及整个代码库保持一致;如果针对你的问题已有既定的工具,请使用该工具。特别是,对于诊断输出,日志记录库通常比 std::cerr 或 std::clog 更好;而对于字符串处理,absl/strings 中的库或其等效库通常比 std::stringstream 更优。
避免将流用于面向外部用户或处理不可信数据的 I/O 操作。相反,应寻找并使用适当的模板库来处理国际化、本地化和安全强化等问题。
如果你确实使用流,请避免使用流 API 中具有状态特性的部分(错误状态除外),例如 imbue()、xalloc() 和 register_callback()。请使用显式的格式化函数(例如 absl::StreamFormat()),而不是流操纵符或格式化标志,来控制进制、精度或填充等格式化细节。
仅当你的类型代表一个值,且 << 运算符用于输出该值的人类可读字符串表示时,才将其重载为流操作符。避免在 << 的输出中暴露实现细节;如果你需要打印对象内部信息用于调试,请改用命名函数(最常用的惯例是名为 DebugString() 的方法)。
前置递增与前置递减 (Preincrement and Predecrement)
除非你需要后置(postfix)语义,否则请使用递增和递减运算符的前置形式(++i)。
// 推荐:前置递增,效率更高
for (int i = 0; i < 10; ++i)
{
// ...
}
// 迭代器也应使用前置
for (auto it = container.begin(); it != container.end(); ++it)
{
// ...
}
// 仅在需要旧值时才使用后置
int j = i++; // 需要先使用 i 的旧值,然后再递增定义:
当对一个变量进行递增(++i 或 i++)或递减(--i 或 i--)操作,且该表达式的返回值不会被使用时,你必须决定是使用前置形式还是后置形式。
优点:
后置递增/递减表达式的求值结果是变量修改之前的值。这虽然能使代码更加紧凑,但往往降低了可读性。相比之下,前置形式通常更加清晰易读。而且,前置形式的效率绝不低于后置形式,通常还更高,因为它无需像后置形式那样,为了保存修改前的旧值而进行一次额外的拷贝。
缺点:
在 C 语言中形成了一种传统,即使在不使用表达式值的情况下(尤其是在 for 循环中),也习惯使用后置递增。
结论:
除非代码明确需要后置递增/递减表达式的结果,否则请使用前置递增/递减。
关于 const 的使用
在 API 中,只要合理就应使用 const。对于 const 的某些用法,constexpr 是更优的选择。
定义:
声明的变量和参数可以用关键字 const 修饰,以表明这些变量的值不会被改变(例如,const int foo)。类的成员函数也可以使用 const 限定符,以表明该函数不会修改类成员变量的状态(例如,class Foo { int Bar(char c) const; };)。
优点:
让使用者更易理解变量的用途。
便于编译器进行更完善的类型检查,并有可能生成更高效的代码。
有助于使用者确信程序的正确性,因为他们知道所调用的函数在修改其变量方面受到了限制。
有助于使用者了解在多线程程序中,哪些函数在无需加锁的情况下使用也是安全的。
缺点:
const 具有传染性:如果你将一个 const 变量传递给函数,该函数的原型中也必须包含 const(否则你就需要使用 const_cast)。这在调用库函数时可能成为一个特别棘手的问题。
结论:
我们强烈建议在 API 中(即在函数参数、方法和非局部变量上)只要有意义且准确,就务必使用 const。这能提供一种一致的、主要由编译器验证的文档说明,指明某个操作可以修改哪些对象。拥有一种一致且可靠的方法来区分读取和写入操作,对于编写线程安全的代码至关重要,在许多其他上下文中也同样有用。具体来说:
如果一个函数能保证不会修改通过引用或指针传入的参数,那么该函数对应的形参应当分别声明为常量引用(const T&)或指向常量的指针(const T*)。
对于按值传递的函数参数,const 对调用者没有任何影响,因此不建议在函数声明中使用。参见 TotW #109。
除非一个成员方法会修改对象的逻辑状态(或者允许用户修改该状态,例如通过返回一个非常量引用,但这种情况很少见),或者该方法不能安全地并发调用,否则都应将其声明为 const。
对局部变量使用 const 既不鼓励也不反对。
类的所有 const 操作都应该能够安全地并发调用。如果这不可行,该类必须被明确地记录为“线程不安全”。
const 的位置
有些人偏爱使用 int const* foo 这种形式,而非 const int* foo。他们认为这样更具可读性,因为它更加一致:它遵循了“const 总是位于它所修饰的对象之后”这一规则。
然而,在指针表达式嵌套不深的代码库中,这个一致性论点并不适用,因为大多数 const 表达式只有一个 const,且它作用于底层的值。在这种情况下,并不存在需要维持的一致性。将 const 放在前面可以说更具可读性,因为它符合英语习惯,将“形容词”(const)置于“名词”(int)之前。
也就是说,尽管我们鼓励将 const 放在前面,但我们并不要求必须这样做。但请务必与你周围的代码保持风格一致!
关于 constexpr、constinit 和 consteval 的使用
这三个关键字都用于强化编译期的约束,但它们的应用场景和含义各有不同。正确使用它们可以显著提升程序的性能和安全性。
1、constexpr (编译时常量与函数)
constexpr 表示“常量表达式”。它告诉编译器,该值或函数的结果在编译期是已知的。
变量: 声明为 constexpr 的变量必须在编译期初始化,且其值不可改变。
函数: 声明为 constexpr 的函数,在传入编译期常量参数时,会在编译期求值;若传入运行时变量,则退化为普通函数在运行时执行。
优点:性能,将计算从运行时转移到编译时,减少运行开销。上下文,可以用在需要编译期常量的上下文中(如数组大小、模板非类型参数)。
constexpr int Square(int x) { return x * x; } constexpr int size = Square(10); // 编译期计算,结果为 100 int arr[size]; // 合法,size 是编译期常量2、constinit (编译期初始化)
constinit 用于确保变量具有静态初始化(零初始化或常量初始化),而不是动态初始化。
含义: 它保证变量在程序启动(main 函数执行前)的静态初始化阶段就被赋予其初始值。
用途: 主要用于解决 C++ 中“跨翻译单元初始化顺序的不确定性”问题,或者确保全局/静态变量的初始化是线程安全的。
注意: constinit 变量不一定是 const 的(即初始化后仍可修改),但它必须在编译期确定初始值。
// 假设在不同的 .cpp 文件中 // file1.cpp extern constinit int global_value; // 声明 // file2.cpp constinit int global_value = 42; // 定义,保证静态初始化3、consteval (立即函数)
consteval 是约束最强的限定符。
含义: 它强制要求函数必须在编译期求值。如果无法在编译期求值,编译将直接报错。
用途: 用于那些绝对不能在运行时执行的函数(例如,生成代码的元编程逻辑、严格的编译期断言)。
consteval int Factorial(int n) { return (n <= 1) ? 1 : (n * Factorial(n - 1)); } constexpr int a = Factorial(5); // 合法,编译期计算 int x = 5; // int b = Factorial(x); // 错误!x 是运行时变量,无法在编译期求值总结与建议
优先使用 const: 用于表示运行时不可变的值。
使用 constexpr: 当值或函数结果可以在编译期确定,并且希望利用编译期计算优化性能时。
使用 constinit: 当你需要确保全局或静态变量在程序启动时就完成初始化(避免动态初始化的陷阱)。
使用 consteval: 当你绝对禁止函数在运行时执行,必须强制在编译期展开时。
使用 constexpr 来定义真正的常量,或确保常量初始化。使用 constinit 来确保非常量变量的常量初始化。
定义:
可以通过将某些变量声明为 constexpr 来表明它们是真正的常量,即在编译/链接时固定不变。可以将某些函数和构造函数声明为 constexpr,从而允许在定义 constexpr 变量时使用它们。可以将函数声明为 consteval,以将其使用限制在编译期。
优点:
使用 constexpr 可以实现以下功能:使用浮点表达式(而不仅仅是字面量)来定义常量;定义用户自定义类型的常量;通过函数调用来定义常量。
缺点:
过早地将某些内容标记为 constexpr,可能会导致后续的迁移问题;如果以后需要将其“降级”,可能会变得很麻烦。当前对 constexpr 函数和构造函数中允许使用的语法所施加的限制,可能会迫使开发者在这些定义中采用晦涩难懂的变通方法。
结论:
constexpr 定义能够更稳健地指定接口中的常量部分。请使用 constexpr 来指定真正的常量以及支持其定义的函数。对于绝对不能在运行时调用的代码,可以使用 consteval。不要为了能够使用 constexpr 而使函数定义变得过于复杂。不要使用 constexpr 或 consteval 来强制函数内联。
整数类型
在 C++ 内置的整数类型中,唯一允许使用的是 int。如果程序需要其他大小的整数类型,请使用 <stdint.h>(或 <cstdint>)中定义的定宽整数类型,例如 int16_t。如果某个值可能大于或等于 2^31,请使用 64 位类型,例如 int64_t。
请记住,即使你的值本身不会超出 int 的范围,但它可能会参与中间计算,而这些计算可能需要更大的类型。如果有任何疑问,优先选择更大的类型。
-

晋公网安备14030302000174号 |