Skip to Content
C++函数(Functions)

函数(Functions)

输入和输出

C++ 函数的输出自然通过返回值提供,有时通过输出参数(或输入/输出参数)提供。

优先使用返回值而不是输出参数:它们提高了可读性,通常提供相同或更好的性能。参见 TotW #176 

优先按值返回,或者如果不行,按引用返回。避免返回原始指针,除非它可以为空。

参数要么是函数的输入,要么是函数的输出,或者两者兼是。非可选输入参数通常应该是值或 const 引用,而非可选输出和输入/输出参数通常应该是引用(不能为空)。一般来说,使用 std::optional 表示可选的按值输入,当非可选形式会使用引用时使用 const 指针。使用非 const 指针表示可选输出和可选输入/输出参数。

避免定义要求引用参数在调用后仍然有效的函数。在某些情况下,引用参数可以绑定到临时对象,导致生命周期错误。相反,找到一种消除生命周期要求的方法(例如,通过拷贝参数),或通过指针传递保留的参数并记录生命周期和非空要求。详见 TotW 116 

排列函数参数时,将所有仅输入参数放在任何输出参数之前。特别是,不要仅仅因为参数是新的就将其添加到函数的末尾;将新的仅输入参数放在输出参数之前。这不是一条硬性规则。既是输入又是输出的参数使情况变得复杂,而且一如既往,与相关函数的一致性可能要求你灵活处理这条规则。可变参数函数也可能需要不寻常的参数排序。

编写简短的函数

优先编写小而专注的函数。

我们认识到长函数有时是合适的,因此没有对函数长度设置硬性限制。如果一个函数超过大约 40 行,请考虑是否可以在不损害程序结构的情况下将其拆分。

即使你的长函数现在运行得很好,几个月后修改它的人可能会添加新的行为。这可能导致难以发现的错误。保持函数简短和简单使其他人更容易阅读和修改你的代码。小函数也更容易测试。

在处理某些代码时,你可能会发现冗长而复杂的函数。不要被修改现有代码吓到:如果处理这样的函数被证明是困难的,你发现错误难以调试,或者你想在几个不同的上下文中使用它的一部分,考虑将函数拆分为更小、更易管理的部分。

函数重载(Function Overloading)

仅当读者查看调用点时能够很好地了解正在发生什么,而不必首先弄清楚正在调用哪个重载时,才使用重载函数(包括构造函数)。

你可以编写一个接受 const std::string& 的函数,并用另一个接受 const char* 的函数重载它。然而,在这种情况下考虑改用 std::string_view

class MyClass { public: void Analyze(const std::string& text); void Analyze(const char* text, size_t textlen); };

重载可以通过允许同名函数接受不同参数来使代码更直观。它对于模板化代码可能是必要的,也可以方便访问者模式。

基于 const 或引用限定的重载可以使工具代码更可用、更高效,或两者兼备。详见 TotW #148 

如果函数仅通过参数类型重载,读者可能必须理解 C++ 复杂的匹配规则才能了解发生了什么。如果派生类只覆盖了函数的某些变体,许多人也会对继承的语义感到困惑。

当变体之间没有语义差异时,你可以重载函数。这些重载可以在类型、限定符或参数数量上有所不同。然而,调用的读者不需要知道选择了重载集中的哪个成员,只需要知道正在调用集合中的某个

为了反映这种统一的设计,优先使用一个全面的”伞形”注释来记录整个重载集,放在第一个声明之前。

如果读者可能难以将伞形注释与特定重载关联起来,为特定重载添加注释也是可以的。

默认参数(Default Arguments)

当默认值保证始终具有相同值时,允许在非虚函数上使用默认参数。遵循与函数重载相同的限制,如果使用默认参数获得的可读性不能超过下面的缺点,则优先使用重载函数。

通常你有一个使用默认值的函数,但偶尔你想覆盖默认值。默认参数提供了一种简单的方式来做到这一点,而不必为罕见的例外定义许多函数。与重载函数相比,默认参数具有更干净的语法,更少的样板代码和更清晰的”必需”与”可选”参数之间的区分。

默认参数是实现重载函数语义的另一种方式,因此所有不重载函数的理由同样适用。

虚函数调用中参数的默认值由目标对象的静态类型决定,而且不能保证给定函数的所有覆盖都声明相同的默认值。

默认参数在每个调用点重新求值,这可能使生成的代码膨胀。读者也可能期望默认值在声明处固定,而不是在每次调用时变化。

在存在默认参数的情况下,函数指针会令人困惑,因为函数签名通常与调用签名不匹配。添加函数重载可以避免这些问题。

虚函数上禁止使用默认参数(它们在虚函数上无法正常工作),以及在指定的默认值可能根据求值时间不同而不会求值为相同值的情况下也禁止。(例如,不要写 void f(int n = counter++);。)

在一些其他情况下,默认参数可以充分提高函数声明的可读性以克服上述缺点,因此是允许的。如有疑问,使用重载。

尾置返回类型语法

仅在使用普通语法(前置返回类型)不实际或可读性差得多的情况下使用尾置返回类型。

C++ 允许两种不同形式的函数声明。在较旧的形式中,返回类型出现在函数名之前。例如:

int Foo(int x);

较新的形式在函数名之前使用 auto 关键字,在参数列表之后使用尾置返回类型。例如,上面的声明可以等价地写为:

auto Foo(int x) -> int;

尾置返回类型在函数的作用域中。对于像 int 这样的简单情况,这没有区别,但对于更复杂的情况很重要,例如在类作用域中声明的类型或用函数参数表述的类型。

尾置返回类型是显式指定 Lambda 表达式返回类型的唯一方式。在某些情况下编译器能够推导 Lambda 的返回类型,但并非总是如此。即使编译器可以自动推导,有时显式指定它对读者来说会更清晰。

有时在函数的参数列表已经出现之后指定返回类型更容易且更具可读性。当返回类型依赖于模板参数时尤其如此。例如:

template <typename T, typename U> auto Add(T t, U u) -> decltype(t + u);

versus

template <typename T, typename U> decltype(declval<T&>() + declval<U&>()) Add(T t, U u);

尾置返回类型语法在 C 和 Java 等类 C++ 语言中没有类似物,因此一些读者可能会觉得不熟悉。

现有代码库中有大量函数声明不会被更改为使用新语法,因此现实的选择是仅使用旧语法或使用两者的混合。使用单一版本更有利于风格的统一。

在大多数情况下,继续使用返回类型在函数名之前的旧式函数声明。仅在必需的情况下(如 Lambda)或通过将类型放在函数参数列表之后使你能以更具可读性的方式编写类型时,才使用新的尾置返回类型形式。后一种情况应该很少见;它主要是相当复杂的模板代码中的问题,而复杂的模板代码在大多数情况下是不鼓励的

Last updated on