使用lambda函数定义非常小的辅助函数是一个好的编程风格吗?

13

举个简单的例子,假设我有一个函数int f(vector<int> v),由于某种原因,我需要在f中多次对v执行一些操作。与其在别处添加一个帮助函数(可能会增加混乱并影响可读性),这样做有什么优缺点(效率、可读性、可维护性等):

int f(vector<int> v)
{
    auto make_unique  = [](vector<int> &v)
    {
        sort(begin(v), end(v));
        auto unique_end = unique(begin(v), end(v));
        v.erase(unique_end, end(v));
    };
    auto print_vector = [](vector<int> const &v)
    {
        copy(begin(v), end(v), ostream_iterator<int>(cout, " "));
        cout << endl;
    };

   make_unique (v);
   print_vector(v);
   // And then the function uses these helpers a few more times to justify making
   // functions...
}

还有其他更好的选择吗?


问题在于我可以用一种方式回答,而其他人可能会给出完全相反的解释。至少试着缩小范围。 - aaronman
我明白你的意思,但我想不出如何重新表达一个风格问题,使它不会变得荒谬,并且有一个“正确答案”。你有什么建议吗? - user904963
不幸的是,我认为这个问题本质上是有主观性的,但如果你把它改成更像“空白有什么优缺点”的形式,那么我会撤销关闭投票,因为这样至少是在询问具体内容。 - aaronman
"sort_unique_erase" 很棒,足以成为一个真正的小男孩。 - Yakk - Adam Nevraumont
1
只是一个小建议,不要将函数命名为make_unique - aaronman
显示剩余2条评论
3个回答

7
这种局部作用域函数的优点在于它们不会在周围代码中污染“辅助”定义,所有行为都可以限制在单个范围内。由于它们可以访问周围函数的词法范围,因此可以用于分解行为而无需传递多个参数。
您还可以使用它们创建小型DSL,以抽象函数的机械细节,从而允许您稍后更改它们。您为重复值定义常量;为什么不对代码做同样的事情呢?
举个小例子,一个状态机:
vector<int> results;
int current;
enum { NORMAL, SPECIAL } state = NORMAL;

auto input   = [&]{ return stream >> current; }
auto output  = [&](int i) { results.push_back(i); };
auto normal  = [&]{ state = NORMAL; };
auto special = [&]{ state = SPECIAL; };

while (input()) {
    switch (state) {
    case NORMAL:
        if (is_special(current)) 
          special(); 
        else output(current);
        break;
    case SPECIAL:
        if (is_normal(current)) 
            normal();
        break;
    }
}

return results;

一个缺点是你可能会不必要地隐藏和专门化一个通用函数,这个函数可能对其他定义也有用。一个“唯一化(uniquify)”或“打印向量(print_vector)”函数值得被提取出来并重复使用。

2

效率:
这基本上是函数与函数对象的比较,而lambda在底层就是一个函数对象。函数对象实际上可以更快,因为它们更容易内联,不是所有编译器都会内联函数指针(尽管可能性存在)。原因是当您传递函数指针时,编译器只知道函数的类型,而函数对象具有整个函数,因为类型是唯一的。

可读性:
这部分或多或少是基于观点的。在我看来,函数指针非常烦人,它们有丑陋的类型,并且不如函数对象灵活。例如,函数对象可以轻松地在运行时传递。另一方面,编写完整函数比大型lambda更易读。

我想要提出的最后一点是,像函数对象一样,lambda可以具有状态(与函数不同,除非您计算静态变量),使用lambda可以捕获变量。由于编译器内联限制,最好不要传递函数指针。

因此,我认为对于传递给stdlib的小函数,最好使用lambda,对于较大的函数则实现函数对象,在C++11中,函数指针不再符合习惯用法。

不幸的是,我认为您使用它们的方式不好。编译器不会将它们内联为普通函数。而名为print_vectormake_unique的函数最好适合于自己的实际函数,因为它可以在许多其他地方使用。

可维护性:
在这种情况下,我认为lambda的可维护性较低,因为它们不能在函数外重用,并且它们看起来很杂乱,因为它们拥挤了所在的函数。因此最好在外部定义它们。


1
OP 没有将这些函数对象传递到任何地方。 - Yakk - Adam Nevraumont
@Yakk 抱歉,我应该实际阅读代码的,我会更改我的答案来解决这个问题,因为它实际上并不好。 - aaronman
@Yakk,你现在可以读完我的答案了。其实他使用它们的方式相当糟糕。 - aaronman
“functor has the whole function”是什么意思?相对于编译器内联,传递函数指针与传递函数对象有何不同? - Bryan Chen
@BryanChen 我也尝试着让我的回答更清晰地解释了那一部分。 - aaronman
显示剩余3条评论

1

C++不支持懒惰。在这里,使用lambda而不是普通函数也不例外。

这个

int f(vector<int> v)
{
    sort(begin(v), end(v));
    v.erase(unique(begin(v), end(v)), end(v));
    copy(begin(v), end(v), ostream_iterator<int>(cout, " "));
    cout << endl;
    /* And then the function uses these helpers a few more times to justify making functions... */
}

比起这个,

更加简洁、标准和易读


int f(vector<int> v)
{
    auto make_unique  = [](vector<int> &v)
    {
        sort(begin(v), end(v));
        auto unique_end = unique(begin(v), end(v));
        v.erase(unique_end, end(v));
    };
    auto print_vector = [](vector<int> const &v)
    {
        copy(begin(v), end(v), ostream_iterator<int>(cout, " "));
        cout << endl;
    };

   make_unique (v);
   print_vector(v);
   /* And then the function uses these helpers a few more times to justify making functions... */
}

作为一个经验法则,当你的代码本身不含晦涩难懂之处(例如erasesortcopy都是标准的C++算法)时,增加更多的代码只会使它变得更糟。
此外:在许多地方没有理由将v的类型限制为vector<int>
在类型不重要的地方使用类型推断。
如果你觉得它应该是一个单独的函数,那就完全将它作为一个单独的函数。
不要用lambda表达式来代替懒惰。

3
@aaronman:这是懒惰的表现,因为它在尖叫:“我认为这应该是一个独立的函数或者函数对象,但我不想真正地将其变成一个,所以我会将其留在这里作为一个 lambda 表达式。”如果它不应该是一个函数,那么它也不应该是一个 lambda。如果它应该是一个函数,那么它就应该是一个函数。Lambda 不是用来组织代码的,它们是用来支持函数式编程(即被传递给其他函数)。 - user541686
2
@Mehrdad:make_uniquev没有任何隐藏依赖。局部作用域定义是代码组织的一种手段,而不是懒惰的证据。 - Jon Purdy
@user904963:嗯,但我的意思是,如果你要重复使用代码,为什么不将其完全作为一个单独的函数呢?make_uniqueprint_vector足够通用,可以被整个程序的其他部分使用,而不仅仅是这个特定的函数。 - user541686
@Mehrdad你只是在回避我们都知道我在问什么的问题。如果你觉得这些函数足够普遍,能够逃脱f的范围,那就假装它们是两个不相关的函数。我还不明白你所说的将一个函数放在范围内比将其放在范围外更懒的说法 - 我可以在f上面输入完全相同数量的字母(略微不同格式),这将神奇地克服我的先前惰性,释放我的工业力量。你是这样认为的吗? - user904963
@user904963:这种情况确实会发生,但并不常见。我没有假定你所说的场景是这种情况,因为经常使用的 lambda 不值得成为一个独立的函数是罕见的。当我看到像make_unique这样的 lambda 时,唯一可以想到的理由就是懒得写完整的版本:template<class T> void make_unique(vector<T> &v) { sort(begin(v), end(v)); v.erase(unique(begin(v), end(v)), end(v)); }; 如果你有一个像这样的例子,它不能成为其自己的函数,那么这将会很有帮助。 - user541686
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接