为什么带有初始化捕获列表的可变 lambda 表达式不能拥有可变数据成员?

3
这个问题与先前的问题有关,其中注意到init-capturing mutable lambda与Boost的range和iterator transform不兼容,因为一些相当模糊和深度嵌套的typedef失败,这些可能很难通过黑客攻击 Boost.Range 源代码来解决。
接受的答案建议将lambda存储在一个std::function对象中。为了避免潜在的虚函数调用开销,我编写了两个函数对象,它们可以作为潜在的解决方法。它们在下面的代码中称为MutableLambda1MutableLambda2.
#include <iostream>
#include <iterator>
#include <vector>
#include <boost/range/adaptors.hpp>
#include <boost/range/algorithm.hpp>

// this version is conforming to the Standard
// but is not compatible with boost::transformed
struct MutableLambda1
{
    int delta;     
    template<class T> auto operator()(T elem) { return elem * delta++; }
};

// Instead, this version works with boost::transformed
// but is not conforming to the Standard
struct MutableLambda2
{
    mutable int delta;
    template<class T> auto operator()(T elem) const { return elem * delta++; }
};

// simple example of an algorithm that takes a range and laziy transformes that
// using a function object that stores and modifies internal state
template<class R, class F>
auto scale(R r, F f) 
{
    return r | boost::adaptors::transformed(f);
}

int main()
{
    // real capturing mutable lambda, will not work with boost::transformed
    auto lam = [delta = 1](auto elem) mutable { return elem * delta++; };        
    auto rng = std::vector<int>{ 1, 2, 3, 4 };

    //boost::copy(scale(rng, lam), std::ostream_iterator<int>(std::cout, ","));                 /* ERROR */
    //boost::copy(scale(rng, MutableLambda1{1}), std::ostream_iterator<int>(std::cout, ","));   /* ERROR */
    boost::copy(scale(rng, MutableLambda2{1}), std::ostream_iterator<int>(std::cout, ","));     /* OK!   */
}

实时示例无法编译带有lamMutableLambda1的行,并且对于带有MutableLambda2的行,正确地打印出1, 4, 9, 16

然而,标准草案提到了

5.1.2 Lambda表达式[expr.prim.lambda]

5[...]如果lambda表达式的参数声明子句后面没有跟随mutable,则此函数调用运算符或运算符模板被声明为const(9.3.1)。[...]

11对于每个init-capture,在闭包类型中声明一个由init-capture的标识符命名的非静态数据成员。该成员不是位域,也不是mutable。[...]

这意味着MutableLambda2不是符合规范的手写替代init-capturing mutable lambda表达式。

问题

  • 为什么init-capturing mutable lambdas的实现方式是这样的(即非const函数调用运算符)?
  • 为什么看似等效的mutable数据成员与const函数调用运算符的替代被禁止?
  • 额外问题)为什么Boost范围和迭代器transform依赖于函数对象的operator()const

抱歉,我可能有点笨,但你能否提供一个非常简短且没有使用boost库的示例,说明你想要做什么但却无法实现? - Kerrek SB
@KerrekSB 这个问题与 Boost.Range 密切相关,恐怕我想要使用一种算法来懒惰地转换一系列数字,该算法需要在函数对象中存储和修改一些内部状态。可变 lambda 初始化似乎是正确的方法,但是会产生类似于模板小说的谚语。MutableLambda2 确实有效,但其实现与标准规定的 lambda 不同。我想知道这是为什么,以及我的解决方法是否有任何隐藏的缺陷。 - TemplateRex
你是在说 MutableLambda2 不符合标准。从哪个方面来说?作为lambda表达式的实现?-- 非const函数调用运算符的可变lambda需要在非const对象上进行调用;另一方面,带有const函数调用运算符和mutable数据成员的lambda则不需要。这是你要找的区别吗? - dyp
@dyp 是的,假设编译器会发出 MutableLambda2 而不是现在所需的 MutableLambda1,那么它会如何破坏用户代码?正如你所指出的,你可以在 const 对象上调用前者,但为什么这样做会有问题呢?(因为它只修改内部状态,而不是传递给 operator() 的参数)。 - TemplateRex
@TemplateRex 或许当您有一个函数对象的const引用时,可以期望一定程度的“纯洁性”。例如,如果算法要求纯函数,因为它可能会调用它多次并得到相同的结果。此外,您可能希望保持两个副本ab一致。当函数调用运算符是const时,您可能合理地假设使用函数a和使用函数b是可互换的。 - dyp
@dyp 关于纯度的问题,你提了个好点子。如果你可以把它写出来,我会把它作为答案接受,并将 MutableLambda2 作为我的问题的一个良好注释的解决方案,直到 Boost 跟上 init-capturing lambdas 的步伐。 - TemplateRex
2个回答

2
template<class L>
struct force_const_call_t {
  mutable L f;
  template<class...Args>
  auto operator()(Args&&...args) const
  { return f(std::forward<Args>(args)...); }
};
template<class L>
force_const_call_t<L> force_const_call(L&&f){
  return {std::forward<L>(f)};
}

上述内容应让您使用force_const_call( ... )将lambda封装,并调用您的boost算法,无需自定义mutable可调用对象(更准确地说,上述操作将lambda转换为自定义的mutable可调用对象)。


哈!我一直在尝试将可变lambda包装在其他lambda中,但这种方法的简单性纯粹是天才。 - TemplateRex
只是一条评论:它可以工作,而且甚至可以高效地工作(在我的应用程序中,所有的包装都被优化掉了),但是通过伪造可变有状态函数对象的常量签名(无论是你的解决方案还是我手写的解决方案),代价是算法不能假设比输入迭代器/单遍范围更强的任何东西。否则,由可变状态引起的顺序依赖性会使您陷入困境。买家自负! - TemplateRex
@TemplateRex 更糟糕的是,该算法可能会复制状态,并使用不同状态的副本评估列表的不同部分。这种算法要求“绕过”要求,需要您知道该算法编写错误(它不应强制进行const调用),但您不被允许重写它。您可以更进一步地编写一个包装器,将 lambda 存储在 std::shared_ptr<L> 中,这意味着现在共享函数对象的副本使用相同的共享状态,这可能是明智的。 - Yakk - Adam Nevraumont

1
正如评论中所指出的,可变 lambda 需要一个非 const 的函数调用运算符,以便让对函数对象的 const 引用表示纯函数。
事实证明,导致我的应用程序出现问题的罪魁祸首是 Boost.Iterator,在 Boost.Range 实现的 boost::adaptors::transformed 中。在 Boost.Iterator 文档的 transform_iterator 的要求 中进行一些挖掘后,发现(加粗强调为我的):
引用:UnaryFunction 必须是可赋值的、可复制构造的,并且表达式 f(*i) 必须有效,其中 f 是类型为 UnaryFunction 的 const 对象,i 是类型为 Iterator 的对象,而 f(*i) 的类型必须为 result_of::reference)>::type。
非纯函数对象需要维护状态,因此不能使用lambda表达式编写,而是需要使用带有const函数调用operator()和带有mutable数据成员表示状态的方式来编写。这也在相关问答中提到过。 注意:这方面存在一个已知的bug报告

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