1

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

8
  • 2
    A capturing Lambda is not convertible to a function pointer see ClosureType::operator ret(*)(params)() here en.cppreference.com/w/cpp/language/lambda "...This user-defined conversion function is only defined if the capture list of the lambda-expression is empty..." Commented Dec 26, 2021 at 13:54
  • I guess my workaround didn't work; I was essentially trying to Pass a class member function as a function parameter stackoverflow.com/questions/7218519/… Commented Dec 26, 2021 at 13:59
  • You can still make it work: make the Observer constructor a template<class Function> and type-erase that into a data member of type std::function<void(int)>. Commented Dec 26, 2021 at 14:00
  • And my limitation was that I couldn't bind to a specific class; the observation method exists in two or more types. Commented Dec 26, 2021 at 14:01
  • @YSC method with this specific signature can exist in two or more class types. the linked question/answer assumes only a single class. Commented Dec 26, 2021 at 14:03

2 Answers 2

2

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) {});
}
Sign up to request clarification or add additional context in comments.

1 Comment

It's nice; I'm just wondering if I can shorten it if possible, for example, to be able to pass member methods directly, test->testMethod. or &test::testMethod and anotherTest->anotherMethod (not sure about the syntax). Instead of my wrapper lambda approach, I mean.
0

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. See Test0() for an example of usage of FuncPtrC().

Try it online!

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

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(bool allow_reuse, Args && ... args) {
        if (!allow_reuse && inner_)
            throw std::runtime_error("Same func ptr slot "
                "initialized two times with same lambda!");
        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, bool allow_reuse = false) {
    static Caller<Tag, T> caller;
    caller.Set(allow_reuse, 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, bool allow_reuse = false) {
    if constexpr(Depth == 0)
        if (idx >= 16)
            throw std::runtime_error(
                "Provided func ptr 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), allow_reuse); \
            else \
                return FuncPtrM<FuncT, I * 16 + i, \
                    Depth - 1>(idx / 16, std::forward<T>(func), allow_reuse); \
            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 std::atomic<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; }, true));
        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 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.