12

我正在尝试编写一个带有接受 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 的传递。当然必须有一种方法来实现这一目标?

恐怕我的模板技巧有点欠缺。普通的模板类和函数很容易,但是用编译器玩愚蠢的虫子有点不合我意。有人可以帮帮我吗?

我知道以前有人问过这个问题的变体,但它们通常侧重于普通函数而不是构造函数;或通过参数重载而不是返回类型。

4

3 回答 3

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++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>在标准库中没有特别处理。它仍然是一个用户定义的类型(在某种意义上它与 eg 不同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检查给定签名的精神——也就是说,它们检查一些给定的参数和一些预期的返回类型。它们不执行自省,因此它们在面对例如重载的函子时表现良好。

于 2013-05-11T20:13:48.340 回答
2

所以有很多方法可以解决这个问题,这需要不同的工作量。它们都不是完全微不足道的。

首先,您可以通过检查T::operator()和/或检查传入类型的签名是否为 type来解压缩签名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)>——它们是交叉调用兼容的。

要确定哪个是“最佳”的,您可以在这里查看我的上一个问题,我们可以在其中获取一堆签名并找到最匹配的。然后进行返回类型检查。这变得越来越复杂了!

如果我们使用该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 {};

尚未经过测试/未编译。

于 2013-05-11T00:59:24.120 回答
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()>or构造函数。std::function<void()>

如果您愿意,我们可以tag<>通过向其添加一个 ctor 来支持转换,从而使其变得更智能。然后返回的函数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 中不能直接工作,但您可以转发到构造函数主体中的初始化函数。

于 2015-07-27T20:37:31.667 回答