通用(多态)lambda 的 C++17 向量
C++17 vector of Generic (Polymorphic) lambdas
C++14 引入了通用 lambda(当在 lambda 的签名中使用 auto 关键字时)。
有没有办法用 C++17 将它们存储在向量中?
我知道这个现有问题,但它不符合我的需要:
这是一个示例代码,说明了我想做什么。 (回答前请先看下备注)
#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;
}
注意事项:
A
、B
、TestRunner
的代码不可更改,只能更改测试用例的代码
- 我不想讨论像这样编写测试代码是好还是错,这是题外话(这里使用测试术语只是为了说明我不能枚举所有的 lambda(为了使用他们的变体类型...))
它遵循一个可能的解决方案(我不推荐,但你明确表示你不想讨论它是好是坏等等)。
根据要求,A
、B
和 TestRunner
没有更改(抛开 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;
}
我找不到在向量中存储 non-homogeneous lambda 的方法,因为它们只有 non-homogeneous 类型。
无论如何,通过定义一个接口(参见 Base
)并使用从给定接口和 lambda 继承的模板 class(参见 Wrapper
),我们可以将请求转发给给定的泛型lambda 并且仍然具有同构接口。
换句话说,解决方案的关键部分是以下 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); }
};
其中可以从 lambda 创建包装器,如下所示:
auto l1 = [](auto &) { /* ... */ };
auto w1 = Wrapper<decltype(l1)>{std::move(l1)};
不幸的是,由于要求不修改 TestRunner
,我不得不使用 std::ref
和 std::reference_wrapper
才能将引用放入向量中。
在 wandbox 上查看。
基本上你想要的是 std::function
.
的扩展
std::function<Sig>
是一个 type-erased 可调用对象,可以对特定签名进行建模。我们想要所有这些功能,但有更多的签名,并且所有这些签名都是可重载的。这变得棘手的是我们需要一个线性的重载堆栈。这个答案假定新的 C++17 规则允许在 using 声明中扩展参数包,并将从头开始分段构建。此外,这个答案并不侧重于在必要时避免所有 copies/movies,我只是在搭建脚手架。此外,还需要更多的 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>
{ };
现在,我们只需要使用所有这些。我们有 top-level function
class 它将有一个 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)...);
}
};
现在我们有一个 multi-signature,类型擦除,具有值语义的函数对象。那么你想要的只是:
std::vector<function<void(A&), void(B&)>> actions;
无法以任何方式、形状或形式存储功能模板。它们不是数据。 (函数也不是数据,但函数指针是)。请注意,有 std::function,但没有 std::function_template。有虚函数,但没有虚函数模板。有函数指针,但没有函数模板指针。这些都是一个简单事实的表现:运行时间没有模板。
通用 lambda 只是一个具有 operator()
成员函数模板的对象。以上所有内容也适用于成员模板。
您可以获得一组有限的 compile-time-determined 模板特化,使其表现得像一个对象,但这与仅具有一组有限的(可能重载的)虚函数或函数指针或其他任何对象的对象没有什么不同.在你的情况下,它相当于拥有一个
std::vector <
std::tuple <
std::function<void(A&)>,
std::function<void(B&)>
>
>
应该可以使用自定义转换函数将通用 lambda 转换为这样的对,或者甚至将 ot 包装在具有 operator() 成员模板的对象中,因此从外部看起来它确实如此正是你想要的——但它只适用于类型 A 和 B,而不适用于其他类型。要添加另一种类型,您必须向元组添加另一个元素。
C++14 引入了通用 lambda(当在 lambda 的签名中使用 auto 关键字时)。
有没有办法用 C++17 将它们存储在向量中?
我知道这个现有问题,但它不符合我的需要:
这是一个示例代码,说明了我想做什么。 (回答前请先看下备注)
#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;
}
注意事项:
A
、B
、TestRunner
的代码不可更改,只能更改测试用例的代码- 我不想讨论像这样编写测试代码是好还是错,这是题外话(这里使用测试术语只是为了说明我不能枚举所有的 lambda(为了使用他们的变体类型...))
它遵循一个可能的解决方案(我不推荐,但你明确表示你不想讨论它是好是坏等等)。
根据要求,A
、B
和 TestRunner
没有更改(抛开 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;
}
我找不到在向量中存储 non-homogeneous lambda 的方法,因为它们只有 non-homogeneous 类型。
无论如何,通过定义一个接口(参见 Base
)并使用从给定接口和 lambda 继承的模板 class(参见 Wrapper
),我们可以将请求转发给给定的泛型lambda 并且仍然具有同构接口。
换句话说,解决方案的关键部分是以下 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); }
};
其中可以从 lambda 创建包装器,如下所示:
auto l1 = [](auto &) { /* ... */ };
auto w1 = Wrapper<decltype(l1)>{std::move(l1)};
不幸的是,由于要求不修改 TestRunner
,我不得不使用 std::ref
和 std::reference_wrapper
才能将引用放入向量中。
在 wandbox 上查看。
基本上你想要的是 std::function
.
std::function<Sig>
是一个 type-erased 可调用对象,可以对特定签名进行建模。我们想要所有这些功能,但有更多的签名,并且所有这些签名都是可重载的。这变得棘手的是我们需要一个线性的重载堆栈。这个答案假定新的 C++17 规则允许在 using 声明中扩展参数包,并将从头开始分段构建。此外,这个答案并不侧重于在必要时避免所有 copies/movies,我只是在搭建脚手架。此外,还需要更多的 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>
{ };
现在,我们只需要使用所有这些。我们有 top-level function
class 它将有一个 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)...);
}
};
现在我们有一个 multi-signature,类型擦除,具有值语义的函数对象。那么你想要的只是:
std::vector<function<void(A&), void(B&)>> actions;
无法以任何方式、形状或形式存储功能模板。它们不是数据。 (函数也不是数据,但函数指针是)。请注意,有 std::function,但没有 std::function_template。有虚函数,但没有虚函数模板。有函数指针,但没有函数模板指针。这些都是一个简单事实的表现:运行时间没有模板。
通用 lambda 只是一个具有 operator()
成员函数模板的对象。以上所有内容也适用于成员模板。
您可以获得一组有限的 compile-time-determined 模板特化,使其表现得像一个对象,但这与仅具有一组有限的(可能重载的)虚函数或函数指针或其他任何对象的对象没有什么不同.在你的情况下,它相当于拥有一个
std::vector <
std::tuple <
std::function<void(A&)>,
std::function<void(B&)>
>
>
应该可以使用自定义转换函数将通用 lambda 转换为这样的对,或者甚至将 ot 包装在具有 operator() 成员模板的对象中,因此从外部看起来它确实如此正是你想要的——但它只适用于类型 A 和 B,而不适用于其他类型。要添加另一种类型,您必须向元组添加另一个元素。