简体   繁体   English

通用(多态)lambda的C ++ 17矢量

[英]C++17 vector of Generic (Polymorphic) lambdas

C++14 introduce generic lambdas (when using the auto keyword in the lambda's signatures). C ++ 14引入了通用的lambda(在lambda的签名中使用auto关键字时)。

Is there a way to store them in a vector with C++17 ? 有没有办法用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? 我知道这个现存的问题,但是它不适合我的需求: 我可以使用模板函数指针的std :: vector吗?

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 不能更改ABTestRunner的代码,只能更改测试用例的代码
  • 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 ...)) 我不想讨论这样的代码测试是好是坏,这是不合时宜的(测试术语在这里仅用于说明我无法枚举所有lambda(以便为它们使用变体类型) ...))

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). 根据要求, ABTestRunner尚未更改(撇开auto并不是TestRunner的有效函数参数,我对此进行了相应设置)。
If you can slightly change TestRunner , the whole thing can be improved. 如果您可以稍微更改TestRunner ,则可以改善整个情况。
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. 我看不到在向量中存储非均质lambda的方法,因为它们只是具有非均质类型。
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. 无论如何,通过定义一个接口(请参见Base )并使用从给定接口和lambda继承的模板类(请参见Wrapper ),我们可以将请求转发给给定的通用lambda,并且仍然具有同质接口。
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: 可以从lambda创建包装器的步骤如下:

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. 不幸的是,由于不修改TestRunner的要求,我不得不使用std::refstd::reference_wrapper才能将引用放入向量中。

See it on wandbox . wandbox上看到它。

Basically what you want is an extension of std::function . 基本上,您想要的是std::function的扩展。

std::function<Sig> is a type-erased callable that can model that particular signature. std::function<Sig>是类型可以擦除的可调用对象,可以对该特定签名进行建模。 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. 该答案假定新的C ++ 17规则允许在using声明中扩展参数包,并将从头开始逐步构建。 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. 另外,还需要更多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. 我们需要一个placeholder<F, Sigs...>来覆盖每个call() 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&) : 假设我们有您的两个签名: void(A&)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. 我们有顶级function类,该类将具有base_placeholder成员,该成员将管理其生存期。

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. 请注意,这里有std :: function,但是没有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. 通用lambda只是具有operator()成员函数模板的对象。 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. 应该可以使用自定义转换函数将通用lambda转换为此类,甚至可以将ot包装在具有operator()成员模板的对象中,因此从外部看,它看起来确实可以满足您的要求- -但仅适用于类型A和B,而没有其他功能。 To add another type you would have to add another element to the tuple. 要添加其他类型,您将必须向元组添加另一个元素。

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

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