繁体   English   中英

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

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

C ++ 14引入了通用的lambda(在lambda的签名中使用auto关键字时)。

有没有办法用C ++ 17将它们存储在向量中?

我知道这个现存的问题,但是它不适合我的需求: 我可以使用模板函数指针的std :: vector吗?

这是示例代码,说明了我想做什么。 (请在回答前查看底部的注释)

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

注意事项:

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

它遵循一个可能的解决方案(我不建议这样做,但是您明确地说过,您不想讨论它是好是坏,等等)。
根据要求, ABTestRunner尚未更改(撇开auto并不是TestRunner的有效函数参数,我对此进行了相应设置)。
如果您可以稍微更改TestRunner ,则可以改善整个情况。
话虽如此,下面是代码:

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

我看不到在向量中存储非均质lambda的方法,因为它们只是具有非均质类型。
无论如何,通过定义一个接口(请参见Base )并使用从给定接口和lambda继承的模板类(请参见Wrapper ),我们可以将请求转发给给定的通用lambda,并且仍然具有同质接口。
换句话说,解决方案的关键部分是以下类别:

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

可以从lambda创建包装器的步骤如下:

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

不幸的是,由于不修改TestRunner的要求,我不得不使用std::refstd::reference_wrapper才能将引用放入向量中。

wandbox上看到它。

基本上,您想要的是std::function的扩展。

std::function<Sig>是类型可以擦除的可调用对象,可以对该特定签名进行建模。 我们希望所有这些功能都具有更多签名,并且所有这些签名都可重载。 这变得棘手的地方是我们需要线性的过载堆栈。 该答案假定新的C ++ 17规则允许在using声明中扩展参数包,并将从头开始逐步构建。 同样,这个答案并不专注于在必要时避免所有副本/电影,我只是在搭建脚手架。 另外,还需要更多SFINAE。


首先,我们需要给定签名的虚拟调用运算符:

template <class Sig>
struct virt_oper_base;

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

还有一些东西可以将它们组合在一起:

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

现在烦人的部分。 我们需要一个placeholder<F, Sigs...>来覆盖每个call() 也许有更好的方法可以做到这一点,但是我能想到的最好的方法是拥有两个类型列表模板参数,并在完成它们时将每个签名从一个签名移到另一个签名:

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

如果我绘制层次结构,这可能更有意义。 假设我们有您的两个签名: 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();

我们需要一种检查给定功能是否满足签名的方法:

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

现在,我们仅使用所有这些。 我们有顶级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)...);
    }    
};

现在,我们有了带有值语义的多签名,类型擦除的函数对象。 您想要的只是:

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

不可能以任何方式,形状或形式存储功能模板。 它们不是数据。 (函数也不是数据,但函数指针是)。 请注意,这里有std :: function,但是没有std :: function_template。 有虚拟功能,但没有虚拟功能模板。 有功能指针,但没有功能模板指针。 这些都是一个简单事实的体现:运行时没有模板。

通用lambda只是具有operator()成员函数模板的对象。 以上所有内容也适用于成员模板。

您可以获得一组有限的,由编译时确定的模板特化特性,使其表现得像一个对象,但这与仅具有有限的(可能是重载的)虚函数,函数指针或其他对象的对象没有什么不同。 在您的情况下,这等效于

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

应该可以使用自定义转换函数将通用lambda转换为此类,甚至可以将ot包装在具有operator()成员模板的对象中,因此从外部看,它看起来确实可以满足您的要求- -但仅适用于类型A和B,而没有其他功能。 要添加其他类型,您将必须向元组添加另一个元素。

暂无
暂无

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

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