如何为模板回调参数强制执行特定签名

时间:2016-09-22 06:43:44

标签: c++ templates callback c++14

我希望能够在使用模板化回调参数时强制执行特定的回调签名。目标有三个:

  1. 在用户呼叫站点获取编译器错误,而不是在内部深处 接受回调的方法。
  2. 遵守最少的意外原则,并阻止用户提供一个回调值,当使用回调的实现不检查其值等(确切的签名匹配)时返回一个值
  3. 尽可能使用惯用的c ++和最佳实践。高效。
  4. 来自C#,我首先尝试模仿Action和Func类,如下所示:

    template<typename ...TArgs> using Action = std::function<void(TArgs...)>;
    template<typename TRet, typename ...TArgs> using Func = std::function<TRet(TArgs...)>;
    

    这样做没问题,除了它使用std :: function,我已经听过这个功能以避免这个简单的&#39;任务(失败#3)。此外,不幸的是,它也未达到我上面的#2要求:

    void DoActionStuff(Action<int, float> callback)
    {
        callback(1, 2.0);
    }
    
    void DoFuncStuff(Func<float, int, float> callback)
    {
        float result = callback(1, 2.0);
        if (result > 100.0) { throw runtime_error("derp"); };
    }
    
    DoActionStuff([](int x, float y) { cout << x << " " << y << endl; }); // OK
    DoActionStuff([](int x, float y) { cout << x << " " << y << endl; return x * y; }); // No error :( Why not?
    
    DoFuncStuff([](int x, float y) { cout << x << " " << y << endl; }); // OK - We get a compiler error right here at the call-site for this one...
    DoFuncStuff([](int x, float y) { cout << x << " " << y << endl; return x * y; }); // OK
    

    如何满足以下所有3项要求?有什么好的特质魔法可以用来为这种方法提供更好的签名来准确传达需要什么类型的回调?

    template<typename TCallback>
    void DoStuff(TCallback callback) { ... } // Unnecessarily burdens the client of this api (they have no idea the signature to use here)
    

5 个答案:

答案 0 :(得分:1)

老实说,我会坚持你的第一直觉,而不是类型别名:

void DoAction(std::function<void(int, float)> );
void DoFuncStuff(std::function<float(int, float)> );

在呼叫网站上可以清楚地看到这些功能所期望的内容,它们已经为您进行了有效性检查。他们不会检查完全签名 - 但这真的是你想要的吗?对DoAction()的可调用对象的任何返回值都将被忽略,对于参数类型将允许隐式转换。但是在C ++的所有地方都是如此。而且,这允许更复杂的结构,如传递通用lambdas:

DoFuncStuff([](auto i, auto f){ return f; }); // really want to disallow this?

你当然可以这样写:

// function pointer case
template <class R, class... Args> std::true_type check_member(R (*)(Args...) );

// function object case
template <class F, class R, class... Args> std::true_type check_member(R (F::*)(Args...) );
template <class F, class R, class... Args> std::true_type check_member(R (F::*)(Args...) const );
template <class R, class... Args, class F> auto check_member(F& ) -> decltype(check_member<F, R, Args...>(&F::operator()));

// failure case
template <class... > std::false_type check_member(...);

template <class F>
void DoAction(F f) {
    static_assert(
        decltype(check_member<void, int, float>(f))::value,
        "invalid signature"
    );

    // body
}

但这不如你原来的想法那么明确。并且您需要check_member内容来正确处理重载和模板operator()

模仿某些例外情况。

答案 1 :(得分:1)

Target Membership效率相当高。它是调用时的两个指针间接,并且只有在大多数高质量C ++标准库实现中传递的对象很大时才分配。 (例如,如果MSVC2015与两个std::function一样大,则不会分配。

如果我要以每像素每帧渲染循环或类似极其时间敏感的情况调用它,我只会对std::string感到害羞。即使是每扫描线也是可以接受的。

所以std::function解决了#1和#3。

至于#2,这是值得怀疑的。大多数隐式转换都是您想要做的。您可能遇到的一种情况是,您不希望忽略返回值。

std::function曾经在传递一个函数返回某些内容时抱怨,但是在标准中明确且有意地改变了它并且丢弃了返回值。通过惯例,std::function<void()>签名可以设置为int(),一切都可以接受。

你不想要那个,这是一个合理的位置。

如果我真的需要,我将void()包起来。

std::function

但我只是接受可以忽略虚假的返回值。

答案 2 :(得分:0)

只要您使用非捕获lambdas,就可以依赖它们衰变为函数指针的事实:

void doActionStuff(void(*callback)(int, double)) { callback(0, 0.); }
int main() { doActionStuff([](int, double) { }); }

否则,如果你想强制执行检查并仍然使用捕获lambda,你可以在函数中放置一个static_assert来验证返回类型和函数原型F

#include <type_traits>
#include <utility>

template<typename F>
int doActionStuff(F &&f) {
    static_assert(std::is_same<std::result_of_t<F(int, double)>, int>::value, "!");
    return std::forward<F>(f)(0, 0.);
}

int main() {
    int i = 0;
    doActionStuff([](int, double) { return 42; });
    doActionStuff([i](int, double) { return 42; });
    // these won't work
    // doActionStuff([i](int) { return 42; });
    // doActionStuff([](int, double) { });
}

答案 3 :(得分:0)

您可以使用SFINAE:

template <typename F>
auto DoActionStuff(F&& f) -> decltype(std::declval<int&>() = f(42, 4.2f), void());

答案 4 :(得分:-1)

您可以回滚到函数指针而不是std::function。例如:

template<typename ...TArgs>
using actionPtr = void (*)(TArgs...);

void DoActionStuff(actionPtr<int, float> callback)
{
    callback(1, 2.0);
}

而不是Action,我们会得到:

DoActionStuff([](int x, float y) {
    cout << x << " " << y << endl;
}); // OK
DoActionStuff([](int x, float y) {
    cout << x << " " << y << endl; return x * y;
}); // Woohoo! Now we get error here!

Func的类比声明:

template<typename TRet, typename ...TArgs>
using funcPtr = TRet (*)(TArgs...);

它符合您的(1),(2)和(2.5)要求,因为您对(3)的索赔有些争议。在现代C ++中,std::function似乎是more idiomatic而不是函数指针,因为它允许一些special featuresstd::bind以及其他一些功能性东西相结合。此外,它更通用,因为你can convert(隐含地,如果你想要的话)是指向std::function的函数指针,但反之亦然。