介绍:全局变量
全局变量是件坏事。大家都知道吧?
但是你知道为什么吗?我已经问过这个问题,我们当中许多人无法确切解释为什么应该避免使用全局变量。
这不是作用域的问题。确实,全局常数与全局变量具有相同的作用域,但是全局常数通常被视为一件好事,因为它们使您可以在“魔术值”上打上标签。
有人回答说应该避免使用全局变量,因为它们会导致多线程问题。它们确实会引起多线程问题,因为可以从任何函数访问全局变量,并且可以从多个线程同时读写该变量,但是我认为这不是主要问题。因为众所周知,即使程序中只有一个线程,也应避免使用全局变量。
我认为全局变量是一个问题,因为它们损害了函数。
函数可用于将程序(或另一个函数)分解为更简单的元素,因此,它们降低了复杂性,并且是提高代码表达能力的工具。但是,为此,函数必须遵守某些规则。需要尊重的一条规则源于函数的定义:
一个函数获取输入,然后提供输出
听起来很简单,因为事实如此。 为了简单起见,要理解的重要一点是函数必须清楚地显示其输入和输出。 这是全局变量损害了函数的地方。 一旦存在全局变量,其作用域中的每个函数都可能将该全局变量作为输入和/或输出。 这在函数声明中是隐藏的。 因此,该函数具有输入和输出,但不能确切说明它们是什么。 此类函数是不正常的。
请注意,全局常量如何不会出现此问题。 它们不是函数的输入,因为它们不能改变(就像定义中的输入一样),并且它们当然也不是输出,因为函数不能在其中写入。
结果就是,一个函数必须清楚地表达其输入和输出。 这个想法恰好是函数式编程的基础,因此我们可以这样制定准则:
让你的函数“函数式”
这篇文章接下来显示了在C ++中达到这个目标的惯用方式。
指示函数的输入
很简单,通过函数的参数进行输入。 通常,输入是通过传递引用常量参数(const T&)来表示的。 因此,在读取或编写函数原型时,请记住,使用const引用来表示输入。 对于某些类型,输入也可以按值输入(例如,原始类型)。
指示函数的输入-输出
C++ 允许修改函数的输入。这样的参数既是输入又是输出。典型的表现这种类型的方式是使用非const引用(T&)
指示函数的输出
规则是这样:
输出来自于返回类型
Output f(const Input& input);
听起来确实很自然,但是在很多情况下,我们都不愿意这样做,而通常会看到一种更笨拙的方式:将输出作为非const引用(T&)参数,如下所示:
void f(const Input& input, Output& output);
然后,该函数将负责填充此输出参数。
使用此技术有几个缺点:
-
这是不自然的。 输出应按返回类型列出。 使用上面的代码,你最终在调用处会遇到尴尬的语法:
Output output; f(input, output);
对比下面更简单的语句:
Output output = f(input);
当连续调用多个函数时,这将变得更加尴尬。
- 你无法保证该函数实际上将填充输出,
- 默认构造Output类可能没有意义。 在这种情况下,出于可疑的原因,你可能会强制这样做。
如果通过返回类型来产生输出更好,那么为什么每个人都不会一直这样做呢?
导致我们无法这么做的原因有3种。 而且这些问题大多数时间都可以轻松解决。 它们是:性能,错误处理和多重返回类型。
性能
在C语言中,按值返回听起来很愚蠢,因为它会产生对象的副本,而不是复制指针。但是在C ++中,有几种语言机制可以在按值返回时删除副本。例如,返回值优化(RVO)或移动语义。例如,按值返回任何STL容器都会移动它而不是复制它。移动STL容器与复制指针所花的时间差不多。
实际上,你甚至不必掌握RVO或移动语义即可按值返回对象。去做就对了 !在许多情况下,编译器会尽最大努力清除副本,对于并非如此的情况,无论如何,你都有80%以上的概率认为该代码不在性能的关键部分。
只有当你的探查器表明在函数返回期间创建返回值的副本是性能瓶颈时,你才可以考虑通过引用传递输出参数来降低代码性能。即使那样,你仍然可以有其他选择(例如促进RVO或为返回的类型实现移动语义)。
错误处理
有时,在某些情况下某个函数可能无法计算其输出。 例如,某些输入可能会使功能失败。 如果没有输出,可以返回什么?
在这种情况下,某些代码会退回到按引用传递输出的模式,因为该函数不必填充它。 然后,要指示输出是否已填充,该函数将返回布尔值或错误代码,例如:
bool f(const Input& input, Output& output);
这使得调用者的代码笨拙而脆弱:
Output output;
bool success = f(input, output);
if (success)
{
// use output ...
}
对于调用者,最干净的解决方案是让函数在失败时引发异常,并在成功时返回输出。 但是,周围的代码必须具有异常安全性,而且许多团队始终不会在其代码中使用异常。
即使这样,仍然存在一种解决方案,可以通过返回类型来输出:使用optional。
您可以在专门的文章中看到有关optional的所有信息,但简而言之,optional <T>表示一个对象,该对象可以是T类型的任何值或为空。 因此,当函数成功时,您可以返回包含实际输出的可选内容,而当函数失败时,您可以仅返回空的可选内容:
boost::optional<Output> f(const Input& input);
请注意,optional在标准化过程中,将在C ++ 17中原生提供。
在调用处:
auto output = f(input); // in C++11 simply write auto output = f(input);
if (output)
{
// use *output...
}
多重返回类型
在C ++中,函数只能返回一种类型。 因此,当一个函数必须返回多个输出时,有时会看到以下模式:
void f(const Input& intput, Output1& output1, Output2& output2);
更糟糕的是,不对称地:
Output1 f(const Input& input, Output2& output2);
仍旧回到通过引用传递输出的可怕模式。
如目前的语言(<C ++ 17)所言,解决此问题并按返回类型产生多个输出的最清晰的解决方案是定义一个将输出组装的新结构:
struct Outputs
{
Output1 output1;
Output2 output2;
};
这会导向更有表现力的声明:
Outputs f(const Input& input);
如果两个输出经常在一起,那么将它们组装在一个实际对象中(带有私有数据和公共方法)甚至是有意义的,尽管并非总是如此。
在C ++ 11中,使用元组是一种更快但不那么清晰的解决方案:
std::tuple<Output1, Output2> f(const Input& input);
然后在调用的地方:
Output1 output1;
Output2 output2;
std::tie(output1, output2) = f(inputs);
这样的缺点是迫使输出有默认的构造函数。 (如果您还不熟悉元组,请放心,在专门的文章中探讨元组时,我们将详细介绍上述工作原理。)
最后一点,这有一个可能会集成到C ++ 17中原生支持返回多个值的语法:
auto [output1, output2] = f(const Input& input);
这将是两全其美。 它称为结构化绑定。 f将在此处返回一个std :: tuple。
结论
总结一下,请尽量使输出作为函数的返回类型。 如果这不切实际,请使用其他解决方案,但请记住,这会损害代码的清晰性和表现力。