c++ 没有std::function可以捕获lambda变量吗?

qv7cva1a  于 2023-01-28  发布在  其他
关注(0)|答案(2)|浏览(185)

不使用std::function是否可以获得lambda的捕获值?我问这个问题是因为我想将捕获的副本放到我自己的内存中,而std::function无法做到这一点,因为它们不支持自定义分配器。
(我认为std::function缺少分配器支持是有原因的,也许在lambda中捕获值背后的逻辑很难实现?但是如果可能的话,我想自己试试。)
背景:我要求学习更多关于C++中的lambda的知识。我想把捕获的值和引用指针放在我作为学术练习编写的线程池系统的过对齐内存中。我也非常喜欢用自动捕获来编写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);
    };
}
pu82cl6c

pu82cl6c1#

检查lambda外部捕获的变量的值看起来不太好,但至少你可以使用一个工厂函数来产生它,它具有唯一的"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;
}

输出:

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

如果只是为了让线程数组处理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;
}

输出:

a=1

如果你在为容器进行自定义分配,你可以只为工厂函数使用第二个参数。下面的例子在堆栈缓冲区上使用placement-new。但是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;
}

输出:

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

jmp7cifd2#

考虑到您在评论中提到的线程池系统,您可以尝试多态方法:

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()
};

现在,您可以将线程例程存储在std::vector中(注意:按值指向的指针将导致对象切片;很可能是std::unique_ptr,根据用例,经典的原始指针也可能适合)。
您甚至可以实现 both 变量,您的线程池管理器可以在线程创建函数中提供一个额外的参数,或者通过重载该函数(左值引用:创建引用 Package 器变量;r值参考:创建值变量,移动-构造它),例如:

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))
        );
    }
}

关于对齐问题:特定的模板示例化将选择适合于模板参数类型的对齐方式,因此您不必考虑任何特殊情况。

相关问题