如何在std::函数的签名上重载构造函数
How to overload constructors on signature of a std::function?
我试图写一个类的重载构造函数接受std::函数对象作为参数,但当然每一个该死的东西可以隐式转换为任何签名的std::函数。这当然很有帮助。
的例子:
class Foo {
Foo( std::function<void()> fn ) {
...code...
}
Foo( std::function<int()> fn ) {
...code...
}
};
Foo a( []() -> void { return; } ); // Calls first constructor
Foo b( []() -> int { return 1; } ); // Calls second constructor
无法编译,因为这两个构造函数本质上是相同的,而且有歧义。当然,这是无稽之谈。我试过enable_if is_same和一堆其他的东西。接受函数指针是不可能的,因为这会阻止有状态lambda的传递。肯定有办法做到这一点吧?
恐怕我的模板制作技能有点欠缺。普通的模板类和函数很容易,但是在编译器上玩一些愚蠢的bug就有点超出我的能力范围了。有人能帮我一下吗?我知道以前有人问过这个问题的变体,但它们通常关注的是普通函数而不是构造函数;或通过参数而不是返回类型进行重载。
以下是一些常见的场景,以及为什么我认为std::function
不适合他们:
struct event_queue {
using event = std::function<void()>;
std::vector<event> events;
void add(event e)
{ events.emplace_back(std::move(e)); }
};
在这种简单的情况下,存储特定签名的函子。从这个角度来看,我的建议似乎很糟糕,不是吗?会出什么问题呢?像queue.add([foo, &bar] { foo.(bar, baz); })
这样的东西工作得很好,类型擦除正是您想要的特性,因为可能会存储异构类型的函子,所以它的成本不是问题。这实际上是在add
的签名中使用std::function<void()>
是可以接受的一种情况。但是请继续往下读!
在将来的某个时刻,你意识到一些事件在被回调时可能会使用一些信息——所以你尝试:
struct event_queue {
using event = std::function<void()>;
// context_type is the useful information we can optionally
// provide to events
using rich_event = std::function<void(context_type)>;
std::vector<event> events;
std::vector<rich_event> rich_events;
void add(event e) { events.emplace_back(std::move(e)); }
void add(rich_event e) { rich_events.emplace_back(std::move(e)); }
};
这样做的问题是,像queue.add([] {})
这样简单的东西只能保证在c++ 14中工作——在c++ 11中,编译器被允许拒绝代码。(最近的libstdc++和libc++是两个在这方面已经遵循c++ 14的实现。)像event_queue::event e = [] {}; queue.add(e);
这样的东西仍然有效!所以,只要你是用c++ 14编写代码,也许使用它是可以的。
然而,即使在c++ 14中,std::function<Sig>
的这个特性也可能并不总是满足您的要求。考虑下面的语句,它现在是无效的,在c++ 14中也会出现:
void f(std::function<int()>);
void f(std::function<void()>);
// Boom
f([] { return 4; });
也有很好的理由:std::function<void()> f = [] { return 4; };
不是错误并且工作正常。返回值被忽略和遗忘。
有时std::function
与模板推导一起使用,就像在这个问题和那个问题中看到的那样。这往往会增加另一层痛苦和艰辛。
简单地说,
std::function<Sig>
在标准库中没有被特别处理。它仍然是一个用户定义的类型(从某种意义上说,它不像int
),它遵循正常的重载解析、转换和模板推导规则。这些规则非常复杂并且彼此交互——对于一个接口的用户来说,他们必须记住这些来传递一个可调用对象给它,这不是一个服务。std::function<Sig>
有一种悲剧性的诱惑,它看起来有助于使界面更简洁、更易读,但只有当你不重载这样的接口时,它才真正有效。
我个人有很多可以根据签名检查类型是否可调用的特性。结合表达性的EnableIf
或Requires
子句,我仍然可以维护一个可接受的可读界面。反过来,结合一些排序的重载,我大概可以实现这样的逻辑:"如果函数函数在没有参数的情况下产生可转换为int
的东西,则调用此重载,否则回退到此重载"。这可能看起来像:
class Foo {
public:
// assuming is_callable<F, int()> is a subset of
// is_callable<F, void()>
template<typename Functor,
Requires<is_callable<Functor, void()>>...>
Foo(Functor f)
: Foo(std::move(f), select_overload {})
{}
private:
// assuming choice<0> is preferred over choice<1> by
// overload resolution
template<typename Functor,
EnableIf<is_callable<Functor, int()>>...>
Foo(Functor f, choice<0>);
template<typename Functor,
EnableIf<is_callable<Functor, void()>>...>
Foo(Functor f, choice<1>);
};
注意,is_callable
精神中的特征检查给定的签名——也就是说,它们检查一些给定的参数和一些预期的返回类型。它们不执行自省,因此它们在面对例如重载的函子时表现良好。
所以有很多方法可以做到这一点,这需要不同数量的工作。没有一个是微不足道的。
首先,您可以通过检查T::operator()
和/或检查它是否为R (*)(Args...)
类型来解包传入类型的签名。
检查签名是否等价。
第二种方法是检测调用兼容性。像这样编写一个trait类:
template<typename Signature, typename F>
struct call_compatible;
是std::true_type
或std::false_type
,取决于decltype<F&>()( declval<Args>()... )
是否可转换为Signature
的返回值。在这种情况下,这将解决您的问题。
现在,如果您重载的两个签名是兼容的,则需要做更多的工作。例如,想象一下,如果你有一个std::function<void(double)>
和std::function<void(int)>
——它们是交叉调用兼容的。
要确定哪一个是"最好的",你可以看看这里我之前的一个问题,我们可以取一堆签名并找出最匹配的。然后执行返回类型检查。这变得越来越复杂了!
如果我们使用call_compatible
解决方案,您最终要做的是:
template<size_t>
struct unique_type { enum class type {}; };
template<bool b, size_t n=0>
using EnableIf = typename std::enable_if<b, typename unique_type<n>::type>::type;
class Foo {
template<typename Lambda, EnableIf<
call_compatible<void(), Lambda>::value
, 0
>...>
Foo( Lambda&& f ) {
std::function<void()> fn = f;
// code
}
template<typename Lambda, EnableIf<
call_compatible<int(), Lambda>::value
, 1
>...>
Foo( Lambda&& f ) {
std::function<int()> fn = f;
// code
}
};
是其他解的一般模式
这是call_compatible
的第一次尝试:
template<typename Sig, typename F, typename=void>
struct call_compatible:std::false_type {};
template<typename R, typename...Args, typename F>
struct call_compatible<R(Args...), F, typename std::enable_if<
std::is_convertible<
decltype(
std::declval<F&>()( std::declval<Args>()... )
)
, R
>::value
>::type>:std::true_type {};
尚未测试/未编译的。
所以我有一个全新的解决方案来解决这个问题,在MSVC 2013中工作,并且不吮吸(就像看着指向operator()
的指针)。
标记调度。
可以携带任意类型的标签:
template<class T> struct tag{using type=T;};
接受类型表达式并生成类型标记的函数:
template<class CallExpr>
tag<typename std::result_of<CallExpr>::type>
result_of_f( tag<CallExpr>={} ) { return {}; }
和使用:
class Foo {
private:
Foo( std::function<void()> fn, tag<void> ) {
...code...
}
Foo( std::function<int()> fn, tag<int> ) {
...code...
}
public:
template<class F>
Foo( F&& f ):Foo( std::forward<F>(f), result_of_f<F&()>() ) {}
};
,现在Foo(something)
转发到std::function<int()>
或std::function<void()>
构造函数,这取决于上下文。
我们可以让tag<>
更智能,支持转换,如果你喜欢,通过添加一个元素到它。然后返回double
的函数将调度到tag<int>
:
template<class T> struct tag{
using type=T;
tag(tag const&)=default;
tag()=default;
template<class U,
class=typename std::enable_if<std::is_convertible<U,T>::value>::type
>
tag( tag<U> ){}
};
注意,这不支持sfina -like Foo
-construction failure。例如,如果你将int
传递给它,它将得到一个硬故障,而不是软故障。
虽然这在VS2012中不能直接工作,但您可以在构造函数的主体中转发初始化函数。
- 通过 C++ 中的重载构造函数初始化未知类型的变量
- C++将带有重载构造函数的对象添加到另一个对象
- 将重载构造函数传递给类之间的函数
- 使用重载构造函数时出现不完整的类型错误
- 在 if 语句中调用重载构造函数失败
- C++ 具有常量数组和initializer_list的重载构造函数
- C++头/实现文件中的默认和重载构造函数?
- 有没有办法用 2D 数组 (int) 重载构造函数?
- C++重载构造函数问题
- C++:使用结构或枚举重载构造函数之间的区别
- 未调用模板成员的重载构造函数
- 如果我们在c++中重载构造函数,那么默认构造函数是否仍然存在
- 如何为显式重载构造函数启用复制初始化
- 执行重载构造函数的原因和时间
- c++ 在对象组合上使用重载构造函数
- 按参数C++类型专门化重载构造函数
- 具有公共重载构造函数的私有枚举
- 如何处理默认构造函数和重载构造函数之间的歧义
- 变量不会在重载构造函数C++后保持声明状态
- 对重载构造函数 C++ 的不明确调用