Lambdas只是运算符()重载的类?

时间:2017-08-23 05:35:50

标签: c++ function lambda functor

我读到的关于lambdas的越多,我越是听到人们认为它们只是伪装的功能对象/仿函数(除非它们不能捕获任何东西,在这种情况下它们只是自由静态函数。我想在本地范围内编写lambdas并将它们传递给一个通用事件处理程序,它根据需要调用它们,我开始注意到我几乎无法做任何传统函数对象让我做的事情。请让我知道我对此的理解是否错误,因为我已经评论了一大堆你可以用仿函数做的东西,并且不能用lambdas,据我所知:

#include <iostream>
#include <vector>

struct MyFunctorClass
{
    // Custom constructor, can't do with lambda
    MyFunctorClass(int& capturedVariable) : capturedVariable(capturedVariable) 
        { std::cout << "I can do anything on construction.\n"; }

    // Overloading constructors, different ways to initialise function object, can't do with lambda
    MyFunctorClass(int& capturedVariable, int sizeOfBuffer) : capturedVariable(capturedVariable) 
        { heapAllocation = new int[sizeOfBuffer]; }

    // Custom destructor, can't do with lambda
    ~MyFunctorClass() { delete[] heapAllocation; }  

    void operator()() { std::cout << "Standard call\n"; }
    void operator()(int arg) { std::cout << "Argument passed: " << arg << '\n'; }   
    // operator() overloading, different ways to call the function object, can't do with lambda

    int* heapAllocation;                // Have heap allocated resources, can't do with lambda
    bool internalStateVariable = true;  // Initialise a member variable on construction, can't do with lambda
    int& capturedVariable;              // I can access this variable directly with MyFunctorClass::capturedVariable = 7, can't do with lambda
};

int main()
{
    int localVar = 0;
    bool trueOrFalse = false;

    {
        MyFunctorClass* myFunctionObj = new MyFunctorClass(localVar, 100);  
        // Can dynamically allocate function object, can't with lambda
        auto lambda = new[&]() { localVar = 1; };   // Can't do?
        lambda.trueOrFalse = true;      // trueOrFalse isn't member of lambda, even though it captured it, doesn't make sense

    }   // Lambda object is destroyed here. My function object lives until I delete it.

    return 0;
}

void holdFunctionObject(MyFunctorClass* funcObj)
{
    static std::vector<MyFunctorClass*> list;
    list.push_back(funcObj);    
    // I can hold all the function objects forever, they'll never go out of scope unless I delete them, can't do with lambda
}

我感到非常受限制,似乎lambdas只是一种声明功能的方式&#34;就位#34;。它们也保持状态,但只能保存已经在范围内的对象状态,而不是创建新对象。而且也无法以仿函数的任何特定方式进行初始化。我做对了吗?因为它们与仅具有重载operator();

的类看起来非常不同

3 个答案:

答案 0 :(得分:8)

我不明白你的意思;是的,lambdas只是具有operator()的类实例的语法糖,它遵循一些特定的模板 1 ;是的,你无法完成大部分完全自定义类所做的事情 - 而且有充分的理由:如果你想做那些事情,你已经可以写一个普通的class 了。

Lambdas采用一种特殊的,广泛使用的模式(可能捕获状态的一次性仿函数) 2 并为它们提供非常方便的语法 3 。如果您需要完全成熟的课程,那么自70年代中期以来,该语言已经涵盖class关键字。

此外,重要的是要注意,尽管C ++中的lambdas在类中是实现的,但是作为这两个概念背后的“世界观”是不同的;闭包植根于函数式编程,而不是OOP。它们背后的想法是模拟单个函数(= action)以及在创建站点捕获的工作所需的数据;而对象首先是一个可变实体,它提供一个变异或查询其状态的接口。 closures can be implemented in terms of objects and objects can be implemented in terms of closures is interesting and intriguing(最终来自两者都是状态和代码组合的事实)这一事实,但它们是从完全不同的理由开始的。使用一个或另一个本质上是理解你想要的主要是代码是否恰好捕获状态(“动词”)或主要是“打包状态”,其中有一个用于改变它的界面(“名词”)。

  1. 这不仅仅是了解其实现的捷径,它们实际上是在标准中的这些术语中指定的。
  2. 来自语言的模式,其中函数和闭包从一开始就是第一类对象,因此产生的语法糖有助于编写从这些语言变异的样式的代码。
  3. 让我告诉你,这是一种祝福;由于不得不将C ++ 11代码移植到C ++ 03,所以去除代码是一种令人沮丧的练习 - 一切都变得如此繁琐冗长,并且在阅读时结果“流动”的方式更糟。捕获状态特别可怕,通常你必须至少重复四次相同的事情 - 一个在类级别的字段声明中,一个在构造函数原型中,一个在初始化列表中,一个在构造函数调用中。与lambda捕获列表比较。

答案 1 :(得分:6)

  

除非它们没有捕获任何东西,在这种情况下它们只是自由静态函数

这实际上是不正确的。无捕获的lambda可以转换到函数指针中,但它们与“自由静态函数”相同。它们仍然是对象,而静态函数则不是(虽然你可以有函数指针,但不能使函数成为C ++意义上的对象)。

至于其余部分,你似乎没有认识到“所有lambdas都是仿函数”和“所有仿函数都是lambdas”之间的区别。前者是真的,后者不是,后者意图也不是真的。

Lambdas私下拥有自己的州。手写的仿函数可以随心所欲地保持状态。大多数人会认为具有可公开访问状态的仿函数是不好的形式,但如果这就是你想要的,那么这是一个由你决定的决定。

使用lambda,即表示您同意接受其限制。 lambda的类型在声明点处被合成,并且它没有被命名,因此你不能创建接受特定lambda的API(好吧,也许你可以,但你不应该)。如果您希望用户传递特定的仿函数类型,那么您不应该使用lambda。

但实际上......您经常需要用户传递特定的仿函数类型吗?大多数情况下,您需要的是让用户传递可以使用特定签名调用的内容。这就是std::function的用途。或者,如果它是模板函数,则可以使用可使用特定签名调用的任意对象。

如果你确实需要用户传递特定的仿函数类型......那就这样吧。但由于大多数接口不会受到如此限制,因此lambda是一种有效的解决方案。

Lambdas的核心是语法糖,可以很容易地编写常见的函子。肯定会有一些lambda无法满足您的需求。但由于lambda占据了大约90%的用途,这使得它们足够好。

lambdas的主要目的是逻辑地将某个进程的代码放在您要调用该进程的位置附近。这提供了相对于该进程的代码的位置。这对于回调,延续等很常见。

与手写的仿函数相比,它还允许此类代码轻松访问回调的本地范围。这使得这些过程成为代码的一部分,而不是必须追踪仿函数类型的定义并阅读其operator()实现来弄清楚发生了什么。

此外:

auto lambda = new[&]() { localVar = 1; };   // Can't do?

你可以这样做:

auto lambda = new auto([&]() { localVar = 1; });   // Can't do?

答案 2 :(得分:2)

你有多点说你不能用lambda做,但实际上你可以。

  

在构造时初始化成员变量,不能用lambda

好吧,从c ++ 14开始,捕获init就是一件事:

// internalState is a bool
auto l = [internalState = true] {};
  

有堆分配资源,不能用lambda

这两个例子怎么样?

// Raw owning pointers are bad, don't do this
auto l1 = [rintptr = new int] {
    // rintptr is a captured pointer, with heap allocated data
    delete rintptr;
};

// Unique pointer is better in every way.
auto l2 = [uptr = std::make_unique<int>()] {
    // uptr is a captured heap allocated int too
};

对于自定义析构函数,没有任何东西可以。每个捕获的对象都应自动清理。如果没有,请创建一个RAII包装器。

对于自定义构造函数,它无法想象一个好的用例而不会进一步使lambda复杂化。请记住,lambdas并不是要完全替换类,而是提供一种简单的方法来声明经典仿函数。

如果在构造特定lambda时确实需要执行某些操作,则可以使用两个lambda,一个返回另一个:

auto l = []{
    // Execute what you need before returning the lambda
    // Setup objects to be captured here
    return []{};
}();
  

可以动态分配函数对象,不能用lambda

这里需要的是std::function。它将正确管理lambda的生命周期。

std::function<void()> func;

{
    // The lambda is moved to func
    func = []{};
}

// func still holds it's instance of the lambda.

此外,std::function很聪明。它甚至可以使用SBO保持lambda,尽可能避免分配,从而提高速度。

  

trueOrFalse不是lambda的成员,即使它捕获了它,也没有意义

那是因为lambda的成员是私人的。而且没关系,lambdas只是为了暴露它的调用操作符。如果你想从外部改变一个成员,你可以使用指针或引用来模拟它。

此外,由于所有lambda都是不同类型的实例,并且捕获往往使用局部变量的名称,因此更改局部变量的名称实际上可能会破坏其他地方的代码,这不是很理想。捕获私有也确保您可以控制捕获的数据。

我建议lambda返回上面的lambda。它可以让你设置捕获数据的值,而不必在以后改变它们。

  

operator()重载

Generic lambda执行基本案例:

auto overloaded = [](const auto& val) {
    std::cout << val << std::endl;
};

overloaded(4); // prints 4
overloaded(6.8); // prints 6.8
overloaded("test"); // prints test

如果你有更复杂的案例,你可以通过继承它们来组成lambdas:

// C++17 for simplicity
template<typename... Ls>
struct overload : Ls... {
    explicit overload(Ls... ls) noexcept : Ls{std::move(ls)}... {}

    using Ls::operator()...;
};

现在你可以重载lambdas:

auto o = overload(
    []{},
    [i = 0](int) {},
    [&](double) {}
);

在这里,您甚至可以为每次重载设置不同的捕获集。

你可以在C ++ 14中实现相同的语法,但overload的实现略有不同,而且有点复杂。

你不能用lambda做什么,但你可以用functor在constexpr上下文中使用它们:

constexpr void foo() {
    []{}(); // Ill formed
}

struct MyFunctor {
    constexpr MyFunctor() = default;

    constexpr void operator()(){}
};

constexpr void bar() {
    MyFunctor{}(); // okay
}

请注意,自C ++ 17以来,此限制已被删除。 Lambdas被允许是constexpr。

今天你不能用lambda做的另一件事就是在模板中进行类型匹配。

auto lambda = [](auto someVec) {
    using T = typename decltype(someVec)::value_type; // ugh...
};

在C ++ 2a中,您可以这样做:

auto lambda = []<typename T>(std::vector<T> someVec) {
    // Yay! Only vectors and got T!
};
相关问题