我正在尝试编写一个带有重载构造函数的类,它接受std :: function对象作为参数,但当然每个该死的东西都可以隐式地转换为任何签名的std :: function 。这自然是非常有用的。
示例:
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的传递。当然必须有办法实现这个目标吗?
我的模板技能有点缺乏,我很害怕。普通的模板类和函数很容易,但使用编译器玩傻傻的玩家有点超出了我的联盟。有人可以帮帮我吗?
我之前已经知道这个问题的变体,但它们通常关注的是正常函数而不是构造函数;或者通过参数而不是返回类型重载。
答案 0 :(得分:5)
以下是一些常见情况,以及为什么我认为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); })
这样的东西运行良好,类型擦除正是你想要的功能,因为可能会存储异构类型的函子,因此它的成本不是问题。事实上,这可以说是std::function<void()>
签名中使用add
可以接受的一种情况。但请继续阅读!
在未来的某个时刻,您会发现某些事件在被召回时可能会使用某些信息 - 所以您尝试:
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 ++编写代码,就可以使用它。
但是,即使使用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
与模板扣除同时使用,如this question和that one所示。这往往会增加一层痛苦和艰辛。
简而言之,std::function<Sig>
未在标准库中进行特殊处理。它仍然是用户定义的类型(在某种意义上它与int
不同),它遵循正常的重载分辨率,转换和模板推导规则。这些规则非常复杂并且彼此交互 - 它不是对接口用户的服务,他们必须记住这些以将可调用对象传递给它。 std::function<Sig>
具有这种悲剧性的诱惑,它看起来有助于使界面简洁,更具可读性,但只要你不重载这样的界面,它就真的只有。
我个人有很多特征可以检查类型是否可以根据签名调用。结合expressive EnableIf
or Requires
clauses我仍然可以保持一个可接受的可读界面。反过来,结合一些ranked overloads,我可以实现这样的逻辑:“如果functor在没有参数的情况下调用时可以转换为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
精神中的特征会检查给定的签名 - 也就是说,它们检查某些给定的参数和一些预期的返回类型。它们不进行内省,因此它们在例如面前表现良好。重载函子。
答案 1 :(得分:2)
所以有很多方法可以解决这个问题,这需要付出不同的工作量。它们都不是完全无足轻重的。
首先,您可以通过检查T::operator()
和/或检查它是否为R (*)(Args...)
类型来解压缩传入类型的签名。
然后检查签名的等效性。
第二种方法是检测呼叫兼容性。写一个这样的特征类:
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)>
- 它们是交叉呼叫兼容的。
要确定哪个是“最佳”,您可以在我之前的问题中查看here,我们可以在这里查看一堆签名并找出哪个匹配最佳。然后进行返回类型检查。这变得越来越复杂了!
如果我们使用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 {};
尚未经过测试/未编译。
答案 2 :(得分:0)
所以我有一个全新的解决方案来解决这个问题,它在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()>
构造函数,具体取决于上下文。
如果您愿意,可以通过添加ctor来支持转换,从而使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> ){}
};
请注意,这不支持类似SFINAE Foo
- 构造失败。也就是说,如果你将int
传递给它,它将会失败,而不是软失败。
虽然这在VS2012中不能直接使用,但您可以转发到构造函数体中的初始化函数。