Home > Back-end >  passing a lambda to a constructor while capturing variables
passing a lambda to a constructor while capturing variables

Time:12-27

I have a compile-time error if I pass a lambda while capturing a variable.

Solutions out there are bound and tied to a specific class but essentially it's a use case with 1 to many class types (Observees). It's a kind of polymorphism.

Edit Richard in the comments pointed out the reason but I wonder if there's a workaround to achieve this, I ran into a blocker with free function pointers (see linked question), any lateral thinking?

Observee1

class Test {
    int _value;
public:
    void testMethod(int value) {
        _value = value   1;
    }
};

Observee2 - A different class but same signature

class AnotherTest {
    int _value;
public:
    void anotherMethod(int value) { // same parameters, different implementation
        _value = value * 2; 
    }
};

Observer

class Observer { 
protected:
    void (*_notify)(int value); // method called at runtime
public:
    explicit Observer(void (*notify)(int value));
};

Attempted solution

auto *test = new Test();
auto *anotherTest = new AnotherTest();

auto *observer = new Observer([](int value) { // works
   
});

auto *observer2 = new Observer([test](int value) { // capturing test throws an error.
    // requirement is to use captured object here. 
    test->testMethod(4);
});

auto *anotherObserver = new Observer([anotherTest](int value) {
    anotherTest->testMethod(5);
});

Error enter image description here

CodePudding user response:

The first comment fundamentally answers your question: A capturing lambda is not convertible to a function pointer unless the capture list is empty.

By using a std::function<void(int)> instead of a function pointer, this limitation is resolved.

#include <functional>

class Test {};
class AnotherTest {};

class Observer { 
protected:
  std::function<void(int)> _notify;
public:
  explicit Observer(std::function<void(int)> notify): _notify(notify) {}
};

int main() {
  auto *test = new Test();
  auto *anotherTest = new AnotherTest();

  auto *observer = new Observer([](int value) {});
  auto *observer2 = new Observer([test](int value) {});
  auto *obserer3 = new Observer([anotherTest](int value) {});
}

CodePudding user response:

If you have to use old plain C function pointer and can't migrate to std::function<void(int)> (as suggested in other answer), then I implemented following quite hacky but working solution.

To skip explanations just put a look at usage examples, Test1() function solves exactly your task with your classes, Test0() provides more advanced my own examples.

I created two helper functions - FuncPtr<FuncT>(lambda) accepts single lambda and resulting C function pointer type, this function converts any lambda (even with captures) to plain C style function pointer. This function is designed to be used without loops, if call to this function passes two times then it will return same pointer value even if lambda capture variables are different. Use this function only if this function is called just once, for example at the beginning of main().

Second function FuncPtrM<FuncT>(idx, lambda) is designed to be repeatable. You can call it for same lambda many times. For example in loops or if you call it many times inside other function body. Besides lambda you have to pass runtime index idx which will save this lambda into slot idx. Number of slots is limited but quite large, tens thousands. If you call this function with same lambda and same index then old lambda value will be overwritten into same index.

See Test0() which uses both FuncPtr() and FuncPtrM(), first one is used single time, second is used many time in a loop. Loop example, as you can see in console output (after code), gives each equal value twice, this happens because same slot number is reused twice, hence previous value of lambda in a slot is overwritten.

Updated: Just created third helper function FuncPtrC() that should be probably used always instead of first two functions if you don't want to be confused with first two functions. This function keeps a counter updated by itself and doesn't need any thinking.

Try it online!

#include <vector>
#include <functional>
#include <iostream>
#include <memory>
#include <iomanip>

template <typename Tag, typename InnerFunc>
class Caller {
public:
    template <typename ResT, typename ... Args>
    static ResT Call(Args ... args) {
        return (*inner_)(std::forward<Args>(args)...);
    }
    template <typename ResT, typename ... Args>
    static auto Ptr(ResT (*ptr)(Args...)) {
        return &Call<ResT, Args...>;
    }
    template <typename ... Args>
    static void Set(Args && ... args) {
        inner_ = std::make_shared<InnerFunc>(
            std::forward<Args>(args)...);
    }
private:
    static inline std::shared_ptr<InnerFunc> inner_;
};

template <typename FuncT, typename Tag = FuncT, typename T>
FuncT * FuncPtr(T && f) {
    static Caller<Tag, T> caller;
    caller.Set(std::forward<T>(f));
    return caller.Ptr((FuncT*)nullptr);
}

template <typename FuncT, size_t I = 0, size_t Depth = 2, typename T>
FuncT * FuncPtrM(size_t idx, T && func) {
    if constexpr(Depth == 0)
        if (idx >= 16)
            throw std::runtime_error(
                "Provided table runtime index too large!");
    switch (idx % 16) {
        #define C(i) case i: { \
            if constexpr(Depth == 0) \
                return FuncPtr<FuncT, std::integral_constant< \
                    size_t, I>>(std::forward<T>(func)); \
            else \
                return FuncPtrM<FuncT, I * 16   i, \
                    Depth - 1>(idx / 16, std::forward<T>(func)); \
            break; \
        }
        C( 0) C( 1) C( 2) C( 3) C( 4) C( 5) C( 6) C( 7) C( 8) C( 9)
        C(10) C(11) C(12) C(13) C(14) C(15)
    default:
        throw std::runtime_error(
            "This shouldn't happen! Programming logic error.");
    }
}

template <typename FuncT, typename T>
FuncT * FuncPtrC(T && func) {
    static size_t counter = 0;
    return FuncPtrM<FuncT>(counter  , std::forward<T>(func));
}

void Test0() {
    using F = int (int value);
    int i = 7;
    {
        F * ptr = FuncPtr<F>([i](auto v){ return v   i; });
        std::cout << ptr(10) << std::endl;
    }
    std::cout << std::endl;
    {
        std::vector<F*> funcs;
        for (size_t j = 0; j < 50;   j)
            for (size_t k = 0; k < 2;   k)
                funcs.push_back(FuncPtrM<F>(j,
                    [j, k](auto v){ return v   j * 2   k; }));
        for (size_t i = 0; i < funcs.size();   i) {
            std::cout << std::setfill(' ') << std::setw(3)
                << funcs[i](10) << " ";
            if ((i   1) % 15 == 0)
                std::cout << std::endl;
        }
    }
    std::cout << std::endl;
    {
        std::vector<F*> funcs;
        for (size_t j = 0; j < 50;   j)
            for (size_t k = 0; k < 2;   k)
                funcs.push_back(FuncPtrC<F>(
                    [j, k](auto v){ return v   j * 2   k; }));
        for (size_t i = 0; i < funcs.size();   i) {
            std::cout << std::setfill(' ') << std::setw(3)
                << funcs[i](10) << " ";
            if ((i   1) % 15 == 0)
                std::cout << std::endl;
        }
    }
}

class Test {
    int _value;
public:
    void testMethod(int value) {
        _value = value   1;
    }
};

class AnotherTest {
    int _value;
public:
    void anotherMethod(int value) { // same parameters, different implementation
        _value = value * 2; 
    }
};

class Observer {
public:
    using NotifyFunc = void (int value);
protected:
    void (*_notify)(int value); // method called at runtime
public:
    explicit Observer(void (*notify)(int value)) {
        _notify = notify;
    }
};

void Test1() {
    using NotifyFunc = Observer::NotifyFunc;

    auto *test = new Test();
    auto *anotherTest = new AnotherTest();

    auto *observer = new Observer(FuncPtr<NotifyFunc>(
        [](int value) { // works
        }));

    auto *observer2 = new Observer(FuncPtr<NotifyFunc>(
        [test](int value) { // capturing test throws an error.
            // requirement is to use captured object here. 
            test->testMethod(4);
        }));

    auto *anotherObserver = new Observer(FuncPtr<NotifyFunc>(
        [anotherTest](int value) {
            anotherTest->anotherMethod(5);
        }));
}

int main() {
    Test0();
    Test1();
}

Output:


17

 11  11  13  13  15  15  17  17  19  19  21  21  23  23  25 
 25  27  27  29  29  31  31  33  33  35  35  37  37  39  39 
 41  41  43  43  45  45  47  47  49  49  51  51  53  53  55 
 55  57  57  59  59  61  61  63  63  65  65  67  67  69  69 
 71  71  73  73  75  75  77  77  79  79  81  81  83  83  85 
 85  87  87  89  89  91  91  93  93  95  95  97  97  99  99 
101 101 103 103 105 105 107 107 109 109 
 10  11  12  13  14  15  16  17  18  19  20  21  22  23  24 
 25  26  27  28  29  30  31  32  33  34  35  36  37  38  39 
 40  41  42  43  44  45  46  47  48  49  50  51  52  53  54 
 55  56  57  58  59  60  61  62  63  64  65  66  67  68  69 
 70  71  72  73  74  75  76  77  78  79  80  81  82  83  84 
 85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 
100 101 102 103 104 105 106 107 108 109 
  • Related