简体   繁体   中英

C++17 vector of Generic (Polymorphic) lambdas

C++14 introduce generic lambdas (when using the auto keyword in the lambda's signatures).

Is there a way to store them in a vector with C++17 ?

I know about this existing question, but it doesn't suit my needs : Can I have a std::vector of template function pointers?

Here is a sample code illustrating what I would like to do. (Please see the notes at the bottom before answering)

#include <functional>
#include <vector>

struct A {
    void doSomething() {
        printf("A::doSomething()\n");
    }
    void doSomethingElse() {
        printf("A::doSomethingElse()\n");
    }
};

struct B {
    void doSomething() {
        printf("B::doSomething()\n");
    }
    void doSomethingElse() {
        printf("B::doSomethingElse()\n");
    }
};

struct TestRunner {
    static void run(auto &actions) {
        A a;
        for (auto &action : actions) action(a);
        B b;
        for (auto &action : actions) action(b); // I would like to do it
        // C c; ...
    }
};

void testCase1() {
    std::vector<std::function<void(A&)>> actions; // Here should be something generic instead of A
    actions.emplace_back([](auto &x) {
        x.doSomething();
    });
    actions.emplace_back([](auto &x) {
        x.doSomethingElse();
    });
    // actions.emplace_back(...) ...
    TestRunner::run(actions);
}

void testCase2() {
    std::vector<std::function<void(A&)>> actions; // Here should be something generic instead of A
    actions.emplace_back([](auto &x) {
        x.doSomething();
        x.doSomethingElse();
    });
    actions.emplace_back([](auto &x) {
        x.doSomethingElse();
        x.doSomething();
    });
    // actions.emplace_back(...) ...
    TestRunner::run(actions);
}

// ... more test cases : possibly thousands of them
// => we cannot ennumerate them all (in order to use a variant type for the actions signatures for example)

int main() {
    testCase1();
    testCase2();

    return 0;
}

NOTES :

  • The code of A , B and TestRunner cannot be changed, only the code of the test cases
  • I don't want to discuss if it's good or wrong to code tests like this, this is off-topic (the test terminology is used here only to illustrate that I cannot enumerate all the lambdas (in order to use a variant type for them ...))

It follow a possible solution (that I wouldn't recommend, but you explicitly said that you don't want to discuss if it's good or wrong and so on).
As requested, A , B and TestRunner have not been changed (put aside the fact that auto is not a valid function parameter for TestRunner and I set it accordingly).
If you can slightly change TestRunner , the whole thing can be improved.
That being said, here is the code:

#include <functional>
#include <vector>
#include <iostream>
#include <utility>
#include <memory>
#include <type_traits>

struct A {
    void doSomething() {
        std::cout << "A::doSomething()" << std::endl;
    }
    void doSomethingElse() {
        std::cout << "A::doSomethingElse()" << std::endl;
    }
};

struct B {
    void doSomething() {
        std::cout << "B::doSomething()" << std::endl;
    }
    void doSomethingElse() {
        std::cout << "B::doSomethingElse()" << std::endl;
    }
};

struct Base {
    virtual void operator()(A &) = 0;
    virtual void operator()(B &) = 0;
};

template<typename L>
struct Wrapper: Base, L {
    Wrapper(L &&l): L{std::forward<L>(l)} {}

    void operator()(A &a) { L::operator()(a); }
    void operator()(B &b) { L::operator()(b); }
};

struct TestRunner {
    static void run(std::vector<std::reference_wrapper<Base>> &actions) {
        A a;
        for (auto &action : actions) action(a);
        B b;
        for (auto &action : actions) action(b);
    }
};

void testCase1() {
    auto l1 = [](auto &x) { x.doSomething(); };
    auto l2 = [](auto &x) { x.doSomethingElse(); };

    auto w1 = Wrapper<decltype(l1)>{std::move(l1)};
    auto w2 = Wrapper<decltype(l2)>{std::move(l2)};

    std::vector<std::reference_wrapper<Base>> actions;
    actions.push_back(std::ref(static_cast<Base &>(w1)));
    actions.push_back(std::ref(static_cast<Base &>(w2)));

    TestRunner::run(actions);
}

void testCase2() {
    auto l1 = [](auto &x) {
        x.doSomething();
        x.doSomethingElse();
    };

    auto l2 = [](auto &x) {
        x.doSomethingElse();
        x.doSomething();
    };

    auto w1 = Wrapper<decltype(l1)>{std::move(l1)};
    auto w2 = Wrapper<decltype(l2)>{std::move(l2)};

    std::vector<std::reference_wrapper<Base>> actions;
    actions.push_back(std::ref(static_cast<Base &>(w1)));
    actions.push_back(std::ref(static_cast<Base &>(w2)));

    TestRunner::run(actions);
}

int main() {
    testCase1();
    testCase2();

    return 0;
}

I can't see a way to store non-homogeneous lambdas in a vector, for they simply have non-homogeneous types.
Anyway, by defining an interface (see Base ) and using a template class (see Wrapper ) that inherits from the given interface and a lambda, we can forward the requests to the given generic lambda and still have an homogeneous interface.
In other terms, the key part of the solution are the following classes:

struct Base {
    virtual void operator()(A &) = 0;
    virtual void operator()(B &) = 0;
};

template<typename L>
struct Wrapper: Base, L {
    Wrapper(L &&l): L{std::forward<L>(l)} {}

    void operator()(A &a) { L::operator()(a); }
    void operator()(B &b) { L::operator()(b); }
};

Where a wrapper can be created from a lambda as it follows:

auto l1 = [](auto &) { /* ... */ };
auto w1 = Wrapper<decltype(l1)>{std::move(l1)};

Unfortunately, for the requirement was to not modify TestRunner , I had to use std::ref and std::reference_wrapper to be able to put references in the vector.

See it on wandbox .

Basically what you want is an extension of std::function .

std::function<Sig> is a type-erased callable that can model that particular signature. We want all of that functionality, but with more signatures, and have all of those signatures be overloadable. Where this becomes tricky is that we need a linear stack of overloads. This answer assumes the new C++17 rule allowing expanding parameter packs in a using declaration, and will build up piecewise from the ground up. Also this answer isn't focused on avoiding all the copies/movies where necessary, I'm just building the scaffolding. Also, there needs to be more SFINAE.


First, we need a virtual call operator for a given signature:

template <class Sig>
struct virt_oper_base;

template <class R, class... Args>
struct virt_oper_base<R(Args...)>
{
    virtual R call(Args...) = 0;
};

And something to group those together:

template <class... Sigs>
struct base_placeholder : virt_oper_base<Sigs>...
{
    virtual ~base_placeholder() = default;
    using virt_oper_base<Sigs>::call...;   // <3        
    virtual base_placeholder* clone() = 0; // for the copy constructor
};

Now the annoying part. We need a placeholder<F, Sigs...> to override each of those call() s. There may be a better way to do this, but the best way I could think of is to have two typelist template parameters and just to move each signature from one to the other as we finish with them:

template <class... >
struct typelist;

template <class F, class Done, class Sigs>
struct placeholder_impl;

template <class F, class... Done, class R, class... Args, class... Sigs>
struct placeholder_impl<F, typelist<Done...>, typelist<R(Args...), Sigs...>>
    : placeholder_impl<F, typelist<Done..., R(Args...)>, typelist<Sigs...>>
{
    using placeholder_impl<F, typelist<Done..., R(Args...)>, typelist<Sigs...>>::placeholder_impl;

    R call(Args... args) override {
        return this->f(args...);
    }    
};

template <class F, class... Done>
struct placeholder_impl<F, typelist<Done...>, typelist<>>
    : base_placeholder<Done...>
{
    placeholder_impl(F f) : f(std::move(f)) { }
    F f;
};

template <class F, class... Sigs>
struct placeholder : 
    placeholder_impl<F, typelist<>, typelist<Sigs...>>
{
    using placeholder_impl<F, typelist<>, typelist<Sigs...>>::placeholder_impl;

    base_placeholder<Sigs...>* clone() override {
        return new placeholder<F, Sigs...>(*this);
    }
};

This might make more sense if I draw the hierarchy. Let's say we have your two signatures: void(A&) and void(B&) :

virt_oper_base<void(A&)>       virt_oper_base<void(B&)>
   virtual void(A&) = 0;         virtual void(B&) = 0;
      ↑                          ↑
      ↑                          ↑
base_placeholder<void(A&), void(B&)>
   virtual ~base_placeholder() = default;
   virtual base_placeholder* clone() = 0;
      ↑
placeholder_impl<F, typelist<void(A&), void(B&)>, typelist<>>
   F f;
      ↑
placeholder_impl<F, typelist<void(A&)>, typelist<void(B&)>>
   void call(B&) override;
      ↑
placeholder_impl<F, typelist<>, typelist<void(A&), void(B&)>>
   void call(A&) override;
      ↑
placeholder<F, void(A&), void(B&)>
   base_placeholder<void(A&), void(B&)>* clone();

We need a way to check if a given function satisfies a signature:

template <class F, class Sig>
struct is_sig_callable;

template <class F, class R, class... Args>
struct is_sig_callable<F, R(Args...)>
    : std::is_convertible<std::result_of_t<F(Args...)>, R>
{ };

And now, we just use all of that. We have our top-level function class which will have a base_placeholder member, whose lifetime it manages.

template <class... Sigs>
class function
{   
    base_placeholder<Sigs...>* holder_;
public:
    template <class F,
        std::enable_if_t<(is_sig_callable<F&, Sigs>::value && ...), int> = 0>
    function(F&& f)
        : holder_(new placeholder<std::decay_t<F>, Sigs...>(std::forward<F>(f)))
    { }

    ~function()
    {
        delete holder_;
    }

    function(function const& rhs)
        : holder_(rhs.holder_->clone())
    { }

    function(function&& rhs) noexcept
        : holder_(rhs.holder_)
    {
        rhs.holder_ = nullptr;
    }

    function& operator=(function rhs) noexcept
    {
        std::swap(holder_, rhs.holder_);
        return *this;
    }

    template <class... Us>
    auto operator()(Us&&... us)
        -> decltype(holder_->call(std::forward<Us>(us)...))
    {
        return holder_->call(std::forward<Us>(us)...);
    }    
};

And now we have a multi-signature, type erased, function object with value semantics. What you want then is just:

std::vector<function<void(A&), void(B&)>> actions;

It is not possible to store function templates in any way, shape or form. They are not data. (Functions are not data either, but function pointers are). Notice that there is std::function, but no std::function_template. There are virtual functions, but no virtual function templates. There are function pointers, but no function template pointers. These are all manifestations of a simple fact: there are no templates at run time.

A generic lambda is just an object with an operator() member function template. Everything of the above applies to member templates too.

You can get a finite, compile-time-determined set of template specialisations to behave like an object, but that's no different from an object just having a finite bunch of (possibly overloaded) virtual functions or function pointers or whatever. In your situation, it's an equivalent of having an

std::vector <
    std::tuple <
         std::function<void(A&)>,
         std::function<void(B&)>
    >
 >

It should be possible to convert a generic lambda to such a pair with a custom conversion function, or even wrap ot in an object that has an operator() member template, so from the outside it would look like it does exactly what you want --- but it will only work with types A and B and nothing else. To add another type you would have to add another element to the tuple.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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