如何管理需要从递归 functors/lambdas 派生的模板参数的声明?
How do I manage declarations that require template parameters derived from recursive functors/lambdas?
我正在尝试构建一个干净整洁的具有递归能力的 lambda 自我作用域实现(它基本上是一个 Y 组合器,尽管我认为技术上不完全是)。这是一段带我去的旅程,其中包括 this thread and and this thread.
我已经尽可能清楚地归结了我的一个问题:我如何传递以 lambda 为模板参数的模板仿函数?
#include <string>
#include <iostream>
#define uint unsigned int
template <class F>
class Functor {
public:
F m_f;
template <class... Args>
decltype(auto) operator()(Args&&... args) {
return m_f(*this, std::forward<Args>(args)...);
}
};
template <class F> Functor(F)->Functor<F>;
class B {
private:
uint m_val;
public:
B(uint val) : m_val(val) {}
uint evaluate(Functor<decltype([](auto & self, uint val)->uint {})> func) const {
return func(m_val);
}
};
int main() {
B b = B(5u);
Functor f = Functor{[](auto& self, uint val) -> uint {
return ((2u * val) + 1u);
}};
std::cout << "f applied to b is " << b.evaluate(f) << "." << std::endl;
}
上面的代码不起作用,Visual Studio 声称 f
(在 b.evaluate(f)
调用中)与参数类型不匹配。
我的假设是 auto & self
不够聪明,无法完成这项工作。我该如何解决这个问题?当它们基本上无法定义时,我如何存储和传递这些东西?这就是为什么我见过的许多 Y 组合器实现都有奇怪的双重包装的原因吗?
任何帮助或解释将不胜感激。
我看到的唯一方法是使 evaluate()
成为模板方法;如果你想确保收到一个 Functor
(但你可以简单地接受一个可调用的:见 ):
template <typename F>
uint evaluate(Functor<F> func) const {
return func(m_val);
}
考虑到每个 lambda 都是不同的类型,您可以使用以下简单代码进行验证
auto l1 = []{};
auto l2 = []{};
static_assert( not std::is_same_v<decltype(l1), decltype(l2)> );
所以将特定的 lambda 类型强加给 evaluate()
是行不通的,因为如果您使用(显然)相同的 lambda 函数调用该方法,调用将不匹配,如下所示例子
auto l1 = []{};
auto l2 = []{};
void foo (decltype(l1))
{ }
int main ()
{
foo(l2); // compilation error: no matching function for call to 'foo'
}
最简单的解决方案是:
uint evaluate(std::function<uint(uint)> func) const {
return func(m_val);
}
更进一步是写 function_view
。
uint evaluate(function_view<uint(uint)> func) const {
return func(m_val);
}
(网上有几十种实现,应该很容易找到)
最简单且运行时效率最高的是:
template<class F>
uint evaluate(F&& func) const {
return func(m_val);
}
因为我们不关心 func
是什么,我们只希望它像鸭子一样嘎嘎叫。如果你想早点检查...
template<class F> requires (std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >)
uint evaluate(F&& func) const {
return func(m_val);
}
template<class F,
std::enable_if_t<(std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >), bool> = true
>
uint evaluate(F&& func) const {
return func(m_val);
}
相似只是更晦涩。
你可以写一个 fixes-signature type-erased Functor
,但我认为这是个坏主意。看起来像:
template<class R, class...Args>
using FixedSignatureFunctor = Functor< std::function<R( std::function<R(Args...)>, Args...) > >;
或者效率稍微高一点
template<class R, class...Args>
using FixedSignatureFunctor = Functor< function_view<R( std::function<R(Args...)>, Args...) > >;
但这太疯狂了;你会想忘记 F
是什么,但你不能忘记 F
!
为了使它完全“有用”,您必须将智能 copy/move/assign 操作添加到 Functor
,如果每个操作中的 F
可以复制它可以复制。
template <class F>
class Functor {
public:
// ...
Functor(Functor&&)=default;
Functor& operator=(Functor&&)=default;
Functor(Functor const&)=default;
Functor& operator=(Functor const&)=default;
template<class O> requires (std::is_constructible_v<F, O&&>)
Functor(Functor<O>&& o):m_f(std::move(o.m_f)){}
template<class O> requires (std::is_constructible_v<F, O const&>)
Functor(Functor<O> const& o):m_f(o.m_f){}
template<class O> requires (std::is_assignable_v<F, O&&>)
Functor& operator=(Functor<O>&& o){
m_f = std::move(o.mf);
return *this;
}
template<class O> requires (std::is_assignable_v<F, O const&>)
Functor& operator=(Functor<O> const& o){
m_f = o.mf;
return *this;
}
// ...
};
(c++20 version, replace requires clauses with std::enable_if_t
SFINAE hack in c++17 及之前)。
如何决定
这里要记住的核心是C++有不止一种多态性,使用错误的种类会让你浪费很多时间。
既有编译时多态,也有运行时多态。当你只需要编译时多态时使用运行时多态是一种浪费。
然后在每个类别中,还有更多的子类型。
std::function
是运行时多态类型擦除正则对象。基于继承的虚函数是另一种运行时多态技术。
您的 Y 组合器正在执行编译时多态性。它改变了它存储的内容并公开了一个更统一的界面。
与该接口对话的事物不关心 Y 组合器的内部实现细节,将它们包含在它们的实现中是抽象失败。
evaluate
接受一个可调用的东西并将其传递给 uint
并期望 return 中有一个 uint
。这就是它关心的。它 不关心 是否传递给 Functor<Chicken>
或函数指针。
让它关心它是一个错误。
如果取std::function
,则执行运行时多态;如果它采用 template<class F>
和 F&&
类型的参数,则它是编译时多态的。这是一个选择,他们是不同的。
任何类型的 Functor<F>
都将合同要求放入其 API 中,根本不应该关心。
我正在尝试构建一个干净整洁的具有递归能力的 lambda 自我作用域实现(它基本上是一个 Y 组合器,尽管我认为技术上不完全是)。这是一段带我去的旅程,其中包括 this thread and
我已经尽可能清楚地归结了我的一个问题:我如何传递以 lambda 为模板参数的模板仿函数?
#include <string>
#include <iostream>
#define uint unsigned int
template <class F>
class Functor {
public:
F m_f;
template <class... Args>
decltype(auto) operator()(Args&&... args) {
return m_f(*this, std::forward<Args>(args)...);
}
};
template <class F> Functor(F)->Functor<F>;
class B {
private:
uint m_val;
public:
B(uint val) : m_val(val) {}
uint evaluate(Functor<decltype([](auto & self, uint val)->uint {})> func) const {
return func(m_val);
}
};
int main() {
B b = B(5u);
Functor f = Functor{[](auto& self, uint val) -> uint {
return ((2u * val) + 1u);
}};
std::cout << "f applied to b is " << b.evaluate(f) << "." << std::endl;
}
上面的代码不起作用,Visual Studio 声称 f
(在 b.evaluate(f)
调用中)与参数类型不匹配。
我的假设是 auto & self
不够聪明,无法完成这项工作。我该如何解决这个问题?当它们基本上无法定义时,我如何存储和传递这些东西?这就是为什么我见过的许多 Y 组合器实现都有奇怪的双重包装的原因吗?
任何帮助或解释将不胜感激。
我看到的唯一方法是使 evaluate()
成为模板方法;如果你想确保收到一个 Functor
(但你可以简单地接受一个可调用的:见
template <typename F>
uint evaluate(Functor<F> func) const {
return func(m_val);
}
考虑到每个 lambda 都是不同的类型,您可以使用以下简单代码进行验证
auto l1 = []{};
auto l2 = []{};
static_assert( not std::is_same_v<decltype(l1), decltype(l2)> );
所以将特定的 lambda 类型强加给 evaluate()
是行不通的,因为如果您使用(显然)相同的 lambda 函数调用该方法,调用将不匹配,如下所示例子
auto l1 = []{};
auto l2 = []{};
void foo (decltype(l1))
{ }
int main ()
{
foo(l2); // compilation error: no matching function for call to 'foo'
}
最简单的解决方案是:
uint evaluate(std::function<uint(uint)> func) const {
return func(m_val);
}
更进一步是写 function_view
。
uint evaluate(function_view<uint(uint)> func) const {
return func(m_val);
}
(网上有几十种实现,应该很容易找到)
最简单且运行时效率最高的是:
template<class F>
uint evaluate(F&& func) const {
return func(m_val);
}
因为我们不关心 func
是什么,我们只希望它像鸭子一样嘎嘎叫。如果你想早点检查...
template<class F> requires (std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >)
uint evaluate(F&& func) const {
return func(m_val);
}
template<class F,
std::enable_if_t<(std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >), bool> = true
>
uint evaluate(F&& func) const {
return func(m_val);
}
相似只是更晦涩。
你可以写一个 fixes-signature type-erased Functor
,但我认为这是个坏主意。看起来像:
template<class R, class...Args>
using FixedSignatureFunctor = Functor< std::function<R( std::function<R(Args...)>, Args...) > >;
或者效率稍微高一点
template<class R, class...Args>
using FixedSignatureFunctor = Functor< function_view<R( std::function<R(Args...)>, Args...) > >;
但这太疯狂了;你会想忘记 F
是什么,但你不能忘记 F
!
为了使它完全“有用”,您必须将智能 copy/move/assign 操作添加到 Functor
,如果每个操作中的 F
可以复制它可以复制。
template <class F>
class Functor {
public:
// ...
Functor(Functor&&)=default;
Functor& operator=(Functor&&)=default;
Functor(Functor const&)=default;
Functor& operator=(Functor const&)=default;
template<class O> requires (std::is_constructible_v<F, O&&>)
Functor(Functor<O>&& o):m_f(std::move(o.m_f)){}
template<class O> requires (std::is_constructible_v<F, O const&>)
Functor(Functor<O> const& o):m_f(o.m_f){}
template<class O> requires (std::is_assignable_v<F, O&&>)
Functor& operator=(Functor<O>&& o){
m_f = std::move(o.mf);
return *this;
}
template<class O> requires (std::is_assignable_v<F, O const&>)
Functor& operator=(Functor<O> const& o){
m_f = o.mf;
return *this;
}
// ...
};
(c++20 version, replace requires clauses with std::enable_if_t
SFINAE hack in c++17 及之前)。
如何决定
这里要记住的核心是C++有不止一种多态性,使用错误的种类会让你浪费很多时间。
既有编译时多态,也有运行时多态。当你只需要编译时多态时使用运行时多态是一种浪费。
然后在每个类别中,还有更多的子类型。
std::function
是运行时多态类型擦除正则对象。基于继承的虚函数是另一种运行时多态技术。
您的 Y 组合器正在执行编译时多态性。它改变了它存储的内容并公开了一个更统一的界面。
与该接口对话的事物不关心 Y 组合器的内部实现细节,将它们包含在它们的实现中是抽象失败。
evaluate
接受一个可调用的东西并将其传递给 uint
并期望 return 中有一个 uint
。这就是它关心的。它 不关心 是否传递给 Functor<Chicken>
或函数指针。
让它关心它是一个错误。
如果取std::function
,则执行运行时多态;如果它采用 template<class F>
和 F&&
类型的参数,则它是编译时多态的。这是一个选择,他们是不同的。
任何类型的 Functor<F>
都将合同要求放入其 API 中,根本不应该关心。