简体   繁体   English

我可以在没有 std::function 的情况下捕获 lambda 个变量吗?

[英]Can I capture lambda variables without std::function?

Is it possible to get the captured values of a lambda without using std::function ?是否可以在不使用std::function的情况下获取 lambda 的捕获值? I'm asking because I want to place the captured copies into my own memory, which std::function cannot do as they don't support custom allocators.我问是因为我想将捕获的副本放入我自己的 memory 中, std::function不能这样做,因为它们不支持自定义分配器。

(I assume allocator support missing for std::function is due to a very good reason, perhaps the logic behind capturing values in a lambda is extremely difficult to implement? But if it's possible, I'd like to try it myself.) (我认为std::function缺少分配器支持是有充分理由的,也许在 lambda 中捕获值背后的逻辑非常难以实现?但如果可能的话,我想自己尝试一下。)

Background: I'm asking to learn more about lambda in C++. I'd like to place the captured values and reference pointers in over-aligned memory for a thread pool system I'm writing as an academic exercise.背景:我要求在 C++ 中了解有关 lambda 的更多信息。我想将捕获的值和引用指针放置在过度对齐的 memory 中,用于我作为学术练习编写的线程池系统。 I also really like the brevity writing lambdas with automatic captures, it'd make for a very easy "job" writing interface I think.我也非常喜欢使用自动捕获的简洁编写 lambda,我认为它会成为一个非常简单的“工作”编写界面。

class MyFunction {
public:

    // I want more than just the function pointer, 
    // I also want the captured value copies or references too
    // I'm unsure how to really accomplish this though.
    MyFunction & operator=( ??? ) {
        ???
    }

};

int main(){
    int captureThis = 1;
    MyFunction func = [=]()->void {
        printf("%i", captureThis);
    };
}

Checking values of captured variables outside of lambda would not look good but at least you can use a factory function to produce it with its unique "lambda" template type (also with help of auto on the final type) so that you can do extra work between what thread calls and what you initialize:检查 lambda 之外的捕获变量的值看起来不太好,但至少您可以使用工厂 function 以其独特的“lambda”模板类型(也在最终类型的 auto 帮助下)生成它,以便您可以做额外的工作在线程调用和初始化之间:

#include <iostream>
#include <thread>
#include <vector>

template <typename F>
struct Callable
{
    Callable(const F && lambda):func(std::move(lambda))
    {
        
    }

    // this is what the std::thread calls
    void operator()()
    {
        // you can check the captured variable
        int out;
        func(out,false);
        std::cout<< "Callable successfully found out the value of captured variable: "<<out <<std::endl;
        func(out,true);
    }
    const F func;
};


template<typename F>
Callable<F> CallableFactory(F&& lambda)
{
    return Callable<F>(std::forward<F>(lambda)); // or std::move(lambda)
}

int main()
{
    // variable to capture
    int a=1;
    
    auto callable = CallableFactory([&](int & outputCapturedValue, bool runNow){
        // not looking good as its not possible from outside (because they are private variables & depends on implementation of C++)
        // if checking the captured variables
        if(!runNow)
        {
            outputCapturedValue = a;
            std::cout << "inside the lambda: a=" << a <<std::endl;
        }
        else
        {
            std::cout<<"algorithm runs"<<std::endl;
        }
    });
    
    
    std::vector<std::thread> threads;
    threads.emplace_back(std::move(callable));
    threads[0].join();
    
    return 0;
}

output: output:

inside the lambda: a=1
Callable successfully found out the value of captured variable: 1
algorithm runs

If its only for having an array of lambdas processed by array of threads, you can use smart-pointers and an extra container struct to box/unbox them during work-distribution:如果它只是为了让线程数组处理 lambda 数组,您可以使用智能指针和额外的容器结构在工作分配期间对它们进行装箱/拆箱:

#include <iostream>
#include <thread>
#include <vector>
#include <memory>


struct ICallable
{
    virtual void operator()()=0;
};

template <typename F>
struct Callable:public ICallable
{
    Callable(const F && lambda):func(std::move(lambda))
    {
        
    }

    // this is what the std::thread calls
    void operator()() override
    {
        func();
    }
    const F func;
};


template<typename F>
std::shared_ptr<ICallable> CallablePtrFactory(F&& lambda)
{
    return std::shared_ptr<ICallable>(new Callable<F>(std::forward<F>(lambda)));
}

struct CallableContainer
{
    std::shared_ptr<ICallable> callable;
    void operator()()
    {
        callable.get()->operator()();
    }        
};

int main()
{
    // variable to capture
    int a=1;

    // simulating work pool
    std::vector<std::shared_ptr<ICallable>> callables;
    callables.push_back(CallablePtrFactory([&](){
        std::cout<< "a="<<a<<std::endl;
    }));

    // simulating worker pool load-balancing
    std::vector<std::thread> threads;
    threads.emplace_back(CallableContainer{ callables[0] });
    threads[0].join();
    
    return 0;
}

output: output:

a=1

If you're after a custom-allocation for the container, you can just use a second parameter for the factory function. Following example uses placement-new on a stack buffer.如果您正在为容器进行自定义分配,则只需为工厂使用第二个参数 function。以下示例在堆栈缓冲区上使用 placement-new。 But still the lambda itself has something else outside of it making container's size not changed by its lambda (just like a function-pointer):但是 lambda 本身还有其他一些东西使得容器的大小不会被它的 lambda 改变(就像一个函数指针):

#include <iostream>
#include <thread>
#include <vector>
#include <memory>


struct ICallable
{
    virtual void operator()()=0;
};

template <typename F>
struct Callable:public ICallable
{
    Callable(const F && lambda):func(std::move(lambda))
    {
        currentSize = sizeof(*this); std::cout<<"current size = "<<currentSize <<" (useful for alignement of next element?)" <<std::endl;
    }

    // this is what the std::thread calls
    void operator()() override
    {
        func();
    }
    int currentSize;
    const F func;
    
};


template<typename F>
std::shared_ptr<ICallable> CallablePtrFactory(F&& lambda, char * buffer)
{
    return std::shared_ptr<ICallable>(
                new (buffer) Callable<F>(std::forward<F>(lambda)),
                [](ICallable *){ /* placement-new does not require a delete! */}
                );
}

struct CallableContainer
{
    std::shared_ptr<ICallable> callable;
    void operator()()
    {
        callable.get()->operator()();
    }        
};

int main()
{
    // variable to capture
    int a=1;

    char buffer[10000];

    // simulating work pool
    std::vector<std::shared_ptr<ICallable>> callables;
    
    
    callables.push_back(
        // observe the buffer for placement-new
        CallablePtrFactory([&](){
            std::cout<< "a="<<a<<std::endl;
        },buffer /* you should compute offset for next element */)
    );

  

    // simulating worker pool load-balancing
    std::vector<std::thread> threads;
    threads.emplace_back(CallableContainer{ callables[0] });
    threads[0].join();
    
    return 0;
}

output: output:

current size = 24 (useful for alignement of next element?)
a=1

Considering the thread pool system you mention in comments then you could try a polymorphic approach:考虑到您在评论中提到的线程池系统,您可以尝试一种多态方法:

class ThreadRoutine
{
protected:
    virtual ~ThreadRoutine() { }
public:
    virtual void run() = 0;
};

template <typename Runner>
class ThreadRoutineT
{
    // variant 1:
    Runner m_runner; // store by value;
    // variant 2:
    std::reference_wrapper<Runner> m_runner;
public:
    void run() override { m_runner(); } // call operator()
};

Now you might store your thread routines in a std::vector (note: pointers to, by value would lead to object slicing; likely std::unique_ptr , possibly, depending on use case, a classic raw pointer might fit, too).现在,您可以将线程例程存储在std::vector中(注意:按值指向的指针将导致 object 切片;可能是std::unique_ptr ,根据用例,经典的原始指针也可能适合)。

You could even implement both variants, your thread pool manager could provide an additional parameter in the thread creation function or maybe even more elegant distinguish by overloading that function (l-value reference: create the reference wrapper variant; r-value reference: create the value variant, move-construct it), eg like:你甚至可以实现这两种变体,你的线程池管理器可以在线程创建中提供一个额外的参数 function 或者通过重载 function 来更优雅地区分(左值引用:创建引用包装器变体;右值引用:创建值变体,移动构造它),例如:

class ThreadManager
{
    template <typename Runner>
    void createThread(Runner& runner)
    {
        // assuming std::vector<std::unique_ptr<ThreadRoutine>>
        m_runners.emplace_back
        (
            // assuming appropriate constructor
            std::make_unique<ThreadRoutineRef>(runner)
        );
        // return some kind of thread handle or start thread directly?
        // thread handle: could be an iterator into a std::list
        // (instead of std::vector) as these iterators do not invalidate
        // if elements preceding in the list are removed
        // alternatively an id as key into a std::[unordered_]map
    }
    template <typename Runner>
    void createThread(Runner&& runner)
    {
        m_runners.emplace_back
        (
            // assuming appropriate constructor
            std::make_unique<ThreadRoutineVal>(std::move(runner))
        );
    }
}

About the alignment issue: The specific template instantiations would select the alignment appropriate for the template argument type, so you wouldn't have to consider anything particular.关于 alignment 问题:特定的模板实例化将 select alignment 适用于模板参数类型,因此您不必考虑任何特别的事情。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM