Fluent C++:让你的函数“函数式”

原文

介绍:全局变量

全局变量是件坏事。大家都知道吧?

但是你知道为什么吗?我已经问过这个问题,我们当中许多人无法确切解释为什么应该避免使用全局变量。

这不是作用域的问题。确实,全局常数与全局变量具有相同的作用域,但是全局常数通常被视为一件好事,因为它们使您可以在“魔术值”上打上标签。

有人回答说应该避免使用全局变量,因为它们会导致多线程问题。它们确实会引起多线程问题,因为可以从任何函数访问全局变量,并且可以从多个线程同时读写该变量,但是我认为这不是主要问题。因为众所周知,即使程序中只有一个线程,也应避免使用全局变量。

我认为全局变量是一个问题,因为它们损害了函数。

函数可用于将程序(或另一个函数)分解为更简单的元素,因此,它们降低了复杂性,并且是提高代码表达能力的工具。但是,为此,函数必须遵守某些规则。需要尊重的一条规则源于函数的定义:

一个函数获取输入,然后提供输出

image.png

听起来很简单,因为事实如此。 为了简单起见,要理解的重要一点是函数必须清楚地显示其输入和输出。 这是全局变量损害了函数的地方。 一旦存在全局变量,其作用域中的每个函数都可能将该全局变量作为输入和/或输出。 这在函数声明中是隐藏的。 因此,该函数具有输入和输出,但不能确切说明它们是什么。 此类函数是不正常的。

请注意,全局常量如何不会出现此问题。 它们不是函数的输入,因为它们不能改变(就像定义中的输入一样),并且它们当然也不是输出,因为函数不能在其中写入。

结果就是,一个函数必须清楚地表达其输入和输出。 这个想法恰好是函数式编程的基础,因此我们可以这样制定准则:

让你的函数“函数式”

这篇文章接下来显示了在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。

结论

总结一下,请尽量使输出作为函数的返回类型。 如果这不切实际,请使用其他解决方案,但请记住,这会损害代码的清晰性和表现力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容