C++0x Lambda编程风格

22

我想知道人们在编写 C++0x lambda 时使用的编码风格。最有趣的问题是在编写捕获列表时应该多么详尽。一方面,语言允许显式列出被捕获的变量,并且根据“明确胜于含蓄”的规则,因此进行详尽的列举以清楚地表明意图是很有意义的。例如:

 int sum;
 std::for_each(xs.begin(), xs.end(), [&sum](int x) { sum += x });
另一个支持显式引用的理由是,因为被引用局部变量的生命周期不会因为被引用而改变(因此,一个 lambda 函数很容易引用到其生命周期已经结束的局部变量),所以通过显式引用可以有助于减少此类错误并跟踪它们。
另一方面,该语言还故意提供了一个捷径,用于自动捕获所有引用的局部变量,因此明显是打算用到它的。有人可能会认为,对于像上面那个例子这样的情况,即使使用自动捕获,情况也非常清楚,并且 lambda 函数的生命周期不会超出周围范围,因此没有理由不使用它。
 int sum;
 std::for_each(xs.begin(), xs.end(), [&](int x) { sum += x });
显然,这不必是全有或全无的,但必须有一些理由来决定何时自动捕获,何时显式进行捕获。有什么想法吗?
同样在相同的问题上,还有何时使用复制方式捕获- [=]和何时使用引用方式捕获- [&] 。复制方式捕获显然更安全,因为没有生命周期问题,因此可以认为每当没有需要改变捕获值(或从其他地方查看对其所做的更改)时,默认情况下应该使用它,并且可以将引用方式捕获视为(潜在的过早)优化,仅在确实有区别的情况下应用。
另一方面,几乎总是更快的捕获-by-reference(特别是因为它通常可以被优化成复制方式,如果后者实际上更快,则适用于小型类型和大多数可内联模板函数,如大多数STL算法),如果lambda永远不会超出其范围(这也是所有STL算法的情况),因此在这种情况下默认使用引用方式捕获是微不足道且无害的优化。
你怎么看?

2
我特别希望得到那些已经在生产代码中使用过Lambda表达式并且有一定经验支持他们观点的人的反馈,但是所有的反馈都受欢迎。 - Pavel Minaev
2
Lambda表达式在许多编译器中并没有真正实现 - 我怀疑除非G++或MSVC++至少支持它们,否则你不会找到太多生产代码。 - bdonlan
2
我正在处理使用VC10 lambda表达式的生产代码(尤其是在新部分中),但我是VS2010团队的开发人员,所以我的情况可能有点特殊...然而,我印象中较新的g ++版本(不是beta!)具有lambda表达式的生产质量实现 - 我错了吗? - Pavel Minaev
1
据我所知,G++目前还没有lambda表达式。虽然有其他一些0x特性,但没有lambda表达式。 - jalf
6
目前的GCC 4.4版本和开发中的4.5版本都不支持lambda函数。请参考http://gcc.gnu.org/projects/cxx0x.html,了解GCC的C++0x功能支持详情。 - sstock
显示剩余3条评论
5个回答

6
我从未听说过“显式优于隐式”规则,并且我不同意这个规则。当然,有些情况下这是正确的,但也有很多情况下它是不正确的。这就是为什么0x在添加类型推断时使用了auto关键字。(以及为什么函数模板参数已经在可能的情况下被推断出来) 有很多情况下隐式更可取。
我还没有真正使用过C++ lambda表达式(除了在VC10 beta中探索),但大多数情况下我会选择后者。
std::for_each(xs.begin(), xs.end(), [&](int x) { sum += x });

我的理由?为什么不这样做呢?它很方便。它有效。而且更容易维护。当我修改lambda的主体时,我不必更新捕获列表。为什么我要对编译器比我更了解的事情做出明确的说明呢?编译器可以根据实际使用的内容来确定捕获列表。

至于按引用还是按值捕获?我会像处理常规函数一样应用相同的规则。如果需要引用语义,请按引用捕获。如果需要复制语义,请按值捕获。如果两者都可以,请优先选择小型类型的值,如果复制很昂贵,则选择引用。

这似乎与设计常规函数时需要做出的选择没有什么不同。

我应该阅读一下有关lambda的规范,但难道明确的捕获列表的主要原因不是你可以按值捕获某些变量,按引用捕获其他变量吗?


没有明确的理由,但是是的,只有使用capture list才有可能实现这一点。 - Pavel Minaev
2
关于“编译器比我更清楚” - 编译器并不知道 lambda 的生命周期。如果你在外部作用域中将 [&] lambda 分配给 std::function,编译器会高兴地让你这样做,然后离开 lambda 作用域 - 但当你实际调用它时,lambda 中的所有引用都将无效... - Pavel Minaev
1
“显式优于隐式”来自Python之禅(http://www.python.org/dev/peps/pep-0020/),但这个原则并不仅适用于Python。 - Pavel Minaev
啊,我觉得我之前在某个地方听过这句话。虽然我从来没有把它当做一个独立的规则听过。但是像几乎所有的规则一样,你也可以太过于字面理解它。Python 中有很多隐含的东西(在某些情况下甚至与“简单胜于复杂”这句话相矛盾)。我认为这个规则在消除歧义方面非常有用(如果存在歧义,不要依赖模糊的隐含规则来解决它,要明确表达你想要的),但是当你的意思已经很明确时,就没有必要再明确地说明了。 - jalf

2

我的第一反应是捕获值提供了与Java的匿名内部类差不多的功能,这是一个已知的事实。但是,当您想要使封闭范围可变时,不必再使用大小为1的数组技巧,而是可以改为捕获引用。然后,您需要将lambda的持续时间限制在referand的范围内。

实际上,我同意您的观点,即处理算法时捕获引用应该成为默认选项,因为我预计这将是使用情况的大部分。在Java中,匿名内部类的常见用途是侦听器。 C ++中可以看到更少的侦听器式接口,因此这是一个较小的需求,但仍然存在。在这种情况下,最好严格遵守值捕获,以避免出现错误的机会。捕获shared_ptr的值可能会成为一个大习语,也许?

但是,我还没有使用过lambda,所以我很可能错过了一些重要信息。


值得注意的是,C++标准库中缺少监听器模式主要是由于其相对较小的规模(尤其是没有UI)。几乎任何UI工具包都以某种形式具有监听器/事件,并且其中一些工具包(例如Gtk--)允许您注册任意函数对象作为侦听器,而不仅仅是函数或方法(例如Qt)。随着std::tr1::function(在C++0x中为std::function)作为标准方式来执行此操作的采用,这可能会变得更加流行。因此,问题将存在。 - Pavel Minaev
1
关于对 shared_ptr 的值捕获,以便捕获的状态能够超出作用域(例如监听器) - 这是一个好观点,事实上我们的代码中已经有了一些这样的情况。 - Pavel Minaev
@Pavel Minaev:你不需要捕获 shared_ptr。你可以通过值来捕获变量(因此它的生命周期超出了作用域),然后声明 lambda 为 mutable,这将允许它改变通过值捕获的变量。 - newacct
@newacct 只有在被捕获的变量是可复制的情况下(例如,您不能以这种方式捕获 unique_ptr),并且如果它没有被超过一个带有状态共享语义的 lambda 捕获,则仅通过值进行捕获才有效。 - Pavel Minaev

0

我在这里看到了一个新的编码标准规则!;)

这有点牵强,但只是为了突出“显式”的优点,请考虑以下内容:

void foo (std::vector<int> v, int x1)
{
  int sum = 0;
  std::for_each (v.begin ()
    , v.end ()
    , [&](int xl) { sum += x1; } 
}

现在,我故意选择了不好的名称等内容,只是为了说明问题。如果我们使用显式捕获列表,则上述代码将无法编译,但目前它可以。

在非常严格的环境(安全关键),我可以看到这样的规则成为编码标准的一部分。


2
虽然你说的是对的,但如果你写了一个for循环,并且执行int xl = *iterator; sum += x1;,你可能会犯同样的错误。在这种情况下,没有人要求显式捕获。在lambda的生命周期仅限于局部范围的情况下,我认为隐式捕获并不比简单的大括号继承周围自动作用域更危险(也不更安全)。当lambda将继续存在时,我认为您需要明确捕获内容(并推断未捕获的内容可以安全地在词法范围结束时死亡)。 - Steve Jessop
1
错误可能存在于其他结构中并不重要。这是lambda的另一个优点,因为您可以比“for循环”更多地减少名称的可见性。名称隐藏/可见性可能会导致微妙的错误。编码标准的目标之一是减少可能引起问题的情况,并且在JSF ++、MISRA C/C++等标准中有关于名称隐藏和减少名称可见性的几个规则。我认为这也是其中之一。 - Richard Corden
但实际上,与等效的函数对象相比,它并没有真正减少可见性,只是防止其被扩展。这不是优势,而是缓解措施。因此,在这一点上,我认为Lambda表达式(非常轻微地)比函数对象更差,因为如果您认为默认捕获很糟糕,那么现在您必须禁止它并监管该禁令。但是,也许比结构化编程更好。 - Steve Jessop
事实上,如果我们更喜欢使用auto nextblock = [&somevar] {thing tmpvar; do_something(somevar,tmpvar); }; nextblock();而不是{thing tmpvar; do_something(somevar,tmpvar);},那么我会完全相信Lambda表达式通过限制除了somevar之外的所有本地变量的可见性,从而提供了与现有结构相比的真正优势。但我认为它们的优点在其他方面... - Steve Jessop
“...与等效函数对象相比,降低了可见性。”我从未暗示过它会这样做,但是隐式捕获增加了在 lambda 函数体内不必要可见的名称数量(与函数对象情况相比)- 我认为这可能被视为“坏事(商标)”,我可以想象在安全关键环境中对隐式行为普遍持反感态度的情况下,会有这样的隐式捕获规则。也许我们应该就此达成不同意见的共识! :) - Richard Corden
我想我刚才误读了你所说的“显式”的优点。这是与不使用显式捕获的lambda相比的优点,而不是与最小化可见性(函数对象)的当前方式相比的优点?因此,我现在明白了你提出编码标准规则的意义,因为它可以防止人们“滥用”lambda以意外增加可见性。谢谢 :-) - Steve Jessop

0

如果方便的话,我会选择显式捕获列表。当你想要捕获很多变量时,(你可能做错了什么并且)可以使用抓取所有[&]捕获列表。

我的看法是,显式捕获列表是理想的选择,应该避免使用隐式变量,只有在实际需要时才使用,以免让人们在输入大量代码时感到烦恼。


0

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