Functions 函数
输入和输出
C++ 函数的输出通常通过返回值提供,有时也通过输出参数(或输入/输出参数)提供。
优先使用返回值而非输出参数:这样能提高代码可读性,并且通常能提供相同甚至更好的性能。参见 TotW #176。
优先按值返回,若不可行则按引用返回。除非返回值可能为空,否则应避免返回原始指针。
参数要么是函数的输入,要么是函数的输出,要么兼具两者特性。非可选输入参数通常应采用值类型或 const 引用。非可选的输出参数及输入/输出参数通常应采用引用(引用天然不能为 null)。一般而言:对于可选的按值输入,应使用 std::optional 来表示;对于可选的输入参数(当其非可选形式本应使用引用时),应使用 const 指针;对于可选的输出参数及可选的输入/输出参数,应使用 非 const 指针。
避免定义那些要求引用参数的生命周期长于函数调用本身的函数。 在某些情况下,引用参数可能会绑定到临时对象上,从而导致生命周期错误。相反,您应该寻找消除生命周期要求的方法(例如通过复制参数),或者通过指针传递需要保持生命周期的参数,并明确记录其生命周期和非空要求。参考: 详见 TotW 116。
在对函数参数进行排序时,应将所有仅作为输入的参数置于任何输出参数之前。特别需要注意的是,不要仅仅因为是新增参数就将其添加到函数末尾;新的仅输入参数应放在输出参数之前。这并非一条死板的规则。对于既是输入又是输出的参数,界限会变得模糊。并且,为了保持与相关函数的一致性,有时可能需要打破这一规则。此外,可变参数函数也可能需要特殊的参数排序。
编写简短的函数
优先使用短小且功能集中的函数。
我们承认长函数有时是合理的,因此并未对函数长度设置硬性限制。如果一个函数超过 40 行左右,您应当思考一下:在不破坏程序结构的前提下,是否可以将其拆分。
即使你现在编写的长函数眼下运行完美,但几个月后,其他人修改它时可能会添加新的逻辑,这可能会导致难以发现的 Bug。保持函数简短精炼,能让其他人更轻松地阅读和修改你的代码。此外,小函数也更容易进行测试。
在处理某些代码时,你可能会遇到那些冗长且复杂的函数。不要害怕修改现有代码:如果你发现使用这样的函数非常困难,或者发现其中的错误很难调试,亦或是你想在多个不同的上下文中复用其中的一部分代码,那么请考虑将该函数拆分为更小、更易于管理的片段。
函数重载
在编程语言(如 C++、Java 等)中,函数重载是指允许在同一作用域内定义多个同名函数,但这些函数的参数列表(参数的类型、个数或顺序)必须不同。编译器根据调用时传入的参数来决定具体调用哪一个函数。
// 计算两个整数的和
int add(int a, int b)
{
return a + b;
}
// 计算两个浮点数的和 (函数名相同,参数类型不同,构成重载)
double add(double a, double b)
{
return a + b;
}仅当读者在查看函数调用处时,无需先弄清楚具体调用的是哪一个重载版本,就能很好地理解代码意图的情况下,才使用函数重载(包括构造函数)。
定义:
你可以编写一个接受 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++ 复杂的匹配规则才能弄清楚到底发生了什么。此外,如果派生类仅重写了某个函数的特定变体(而非全部),许多人都会对继承的语义感到困惑。
结论:
只有当重载的各个版本在语义上没有区别时,你才可以对函数进行重载。这些重载版本可以在类型、限定符或参数数量上有所不同。然而,函数调用者必须无需了解具体调用了重载集中的哪一个成员,而只需知道调用了该集合中的某个函数即可。
为了体现这种统一的设计,建议使用一个单一的、全面的“伞状”注释来记录整个重载集,并将其放在第一个声明之前。
如果读者在将“伞状”注释与特定重载版本关联时存在困难,那么为特定的重载版本添加注释也是可以的。
默认参数
在编程中,默认参数是指在声明或定义函数时,为参数指定一个默认值。如果函数调用时没有为该参数提供实参,编译器就会自动使用这个默认值。
// 声明一个带有默认参数的函数
// 如果不传入 y,它将默认等于 10
int Add(int x, int y = 10) {
return x + y;
}
// 调用示例
Add(5, 3); // 结果为 8 (使用传入的 3)
Add(5); // 结果为 15 (使用默认的 10)对于非虚函数,仅当默认参数的值能保证始终如一时,才允许使用默认参数。此时应遵循与函数重载相同的限制规则;如果使用默认参数所带来的可读性提升,并不能抵消下文所述的弊端,那么请优先选择使用重载函数。
优点:
通常情况下,你会使用带有默认值的函数,但偶尔又希望重写这些默认值。默认参数提供了一种简便的方法来实现这一点,而无需为那些极少出现的特例定义大量函数。与函数重载相比,使用默认参数的语法更加简洁,减少了样板代码,且能更清晰地将“必需”参数与“可选”参数区分开来。
缺点:
默认参数是实现重载函数语义的另一种方式,因此所有不建议重载函数的原因也同样适用于它。
对于虚函数调用,默认参数的值是由目标对象的静态类型决定的,无法保证该函数所有的重写版本都声明了相同的默认值。
默认参数在每次调用时都会被重新求值,这可能会导致生成的代码膨胀。读者也可能期望默认值在声明处就已固定,而不是在每次调用时发生变化。
当存在默认参数时,函数指针的使用会变得令人困惑,因为函数签名往往与调用签名不匹配。相比之下,使用函数重载可以避免这些问题。
结论:
在虚函数中禁止使用默认参数,因为它们无法正常工作;如果指定的默认值在不同时间求值时可能产生不同的结果,也禁止使用。(例如,不要写 void f(int n = counter++);。)
在其他一些情况下,如果使用默认参数能够极大地提升函数声明的可读性,从而抵消上述弊端,则允许使用。如果拿不准,请使用函数重载。
尾随返回类型语法
这是 C++11 引入的一种新的函数声明语法。
在传统的 C 语言风格语法中,返回类型写在函数名之前,而在尾随返回类型语法中,返回类型被移到了参数列表之后,使用 auto 占位符和 -> 符号。
ReturnType FunctionName(Parameters); auto FunctionName(Parameters) -> ReturnType;
仅在使用普通语法(前置返回类型)不切实际或可读性较差时,才使用尾随返回类型。
定义:
C++ 允许两种不同形式的函数声明。在较早的形式中,返回类型位于函数名称之前。例如:
int Foo(int x);
新形式在函数名前使用 auto 关键字,并在参数列表后使用尾随返回类型。例如,上面的声明可以等价地写成:
auto Foo(int x) -> int;
尾随返回类型处于函数的作用域内。对于 int 这样的简单类型,这没有区别,但对于更复杂的类型(例如在类作用域内声明的类型,或根据函数参数定义的类型)来说,这一点至关重要。
这句话解释了为什么尾随返回类型(Trailing Return Type)在 C++ 中是必要的,核心在于作用域(Scope)和可见性(Visibility)。
优点:
尾随返回类型是显式指定 lambda 表达式返回类型的唯一方法。在某些情况下,编译器能够自动推导 lambda 的返回类型,但在并非所有情况都能做到这一点。即使编译器能够自动推导,有时显式地指定返回类型也会让读者看得更清楚。
有时,在函数参数列表之后指定返回类型会更加容易且更具可读性。当返回类型依赖于模板参数时,这种优势尤为明显。例如:
template <typename T, typename U> auto Add(T t, U u) -> decltype(t + u);
对比
template <typename T, typename U> decltype(declval<T&>() + declval<U&>()) Add(T t, U u);
缺点:
尾随返回类型语法在 C、Java 等类似 C++ 的语言中没有对应的形式,因此部分读者可能会觉得它比较陌生。
现有的代码库中包含海量的函数声明,这些声明并不会被修改为使用新语法,所以实际面临的选择要么是仅使用旧语法,要么是混合使用两种语法。相比之下,统一使用一种版本更有利于保持代码风格的一致性。
结论:
在大多数情况下,请继续使用旧式的函数声明方式,即将返回类型置于函数名称之前。仅在必须使用新语法的情况下(例如编写 lambda 表达式时),或当把类型放在函数参数列表之后能让类型声明更具可读性时,才使用新的尾随返回类型形式。后一种情况应当很少出现;这主要涉及相当复杂的模板代码,而这种情况在大多数时候是不推荐的。

晋公网安备14030302000174号 |