如何在std::函数的签名上重载构造函数

How to overload constructors on signature of a std::function?

本文关键字:重载 构造函数 函数 std      更新时间:2023-10-16

我试图写一个类的重载构造函数接受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>有一种悲剧性的诱惑,它看起来有助于使界面更简洁、更易读,但只有当你不重载这样的接口时,它才真正有效。

我个人有很多可以根据签名检查类型是否可调用的特性。结合表达性的EnableIfRequires子句,我仍然可以维护一个可接受的可读界面。反过来,结合一些排序的重载,我大概可以实现这样的逻辑:"如果函数函数在没有参数的情况下产生可转换为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_typestd::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中不能直接工作,但您可以在构造函数的主体中转发初始化函数。