如何为模板运算符()编写尽可能好的is_callable特性

How to write the best possible is_callable trait for templated operator()

本文关键字:尽可能 is 特性 callable 运算符      更新时间:2023-10-16

我有如下定义的is_callable特性:

#ifndef IS_CALLABLE_HPP
#define IS_CALLABLE_HPP
#include <type_traits>
namespace is_callable_detail
{
    struct no   {};
    struct yes  { no x[2]; };
    template<bool CallableArgs, typename Callable, typename ReturnType, typename ...Args>
    struct check_return
    {
        static const bool value = std::is_convertible<decltype(std::declval<Callable>()(std::declval<Args>()...)), ReturnType>::value;
    };
    template<typename Callable, typename ReturnType, typename ...Args>
    struct check_return<false, Callable, ReturnType, Args...>
    {
        static const bool value = false;
    };
}
template<typename Callable, typename Function>
struct is_callable;
template<typename Callable, typename ReturnType, typename ...Args>
struct is_callable<Callable, ReturnType(Args...)>
{
    private:
        template<typename T>
        static is_callable_detail::yes check(decltype(std::declval<T>()(std::declval<Args>()...)) *);
        template<typename T>
        static is_callable_detail::no  check(...);
        static const bool value_args = sizeof(check<Callable>(nullptr)) == sizeof(is_callable_detail::yes);
        static const bool value_return = is_callable_detail::check_return<value_args, Callable, ReturnType, Args...>::value;
    public:
        static const bool value = value_args && value_return;
};
#endif // IS_CALLABLE_HPP

我的问题是如何检测没有参数并且只有返回类型t 的模板运算符()

template<typename T>
T operator()()
{
  // ...
}

template<typename T, typename U>
auto operator()() -> decltype(std::declval<T>() + std::declval<U>())
{
  // ...
}

我知道这种情况很少见,但我想问一下,是否有任何方法可以检测不带参数、带一个或多个模板参数的模板运算符()的存在。

如果事先知道operator()不会过载,可以尝试获取其地址。如果operator()可能过载,则正结果将意味着存在operator(),但负结果将意味着您不存在operator(),或者至少存在两个过载。

请注意,模板将(如预期的那样)带来operator()的几个重载。但是,如果您知道非默认模板参数的数量,您可以尝试获取operator()<T>的地址(对于希望不会触发SFINAE的某些类型的T)。

最后,我建议不要花太多时间在不知道要传递什么参数的情况下检查函子(或成员函数,出于同样的原因),就像你已经拥有的一样。C++11使得编写和使用在表达式级别起作用的泛型代码变得非常容易。

您试图检测一个具有非推导模板参数的operator()成员函数模板,这根本不是真正的"可调用",也有点毫无意义-函数模板应该有一个真实的名称,因为您的示例确实没有抓住整个operator的要点。但无论如何,让我们来解决你的问题。

请允许我在前面介绍一个我正在开发的库解决方案的插件,名为CallableTraits(也是一项正在进行的工作)。

虽然您的案例不是由CallableTraits处理的,但库确实使用了我将要描述的一种技术来解决一个非常类似的问题。这项技术完全是黑客攻击,但它符合标准,适用于以下平台:

  • GCC 5.2及更高版本
  • Clang 3.5及更高版本
  • Visual Studio 2015更新1-基本工作

注意:Visual Studio 2015 Update 2已损坏,因为它在部分专业化中错误地推导了std::index_sequence<I...>。。。我提交了一份错误报告。有关说明,请参见此处。

注意:如果您的标准库实现还没有std::disjunction,那么您可以在此处使用示例实现。

我称这种技术为模板蠕虫。这是元编程,相当于向一口又深又黑的井里吐口水,只是为了听听它需要多长时间才能吐出来。

什么是模板蠕虫?

  1. 模板蠕虫是一个可以转换为任何内容的类
  2. 任何具有模板蠕虫操作数的运算符表达式的计算结果都将始终为另一个模板蠕虫
  3. 模板蠕虫只能在未评估的上下文中使用。换句话说,只有当decltype围绕顶级表达式时,才能使用它,就像std::declval<T>()一样

模板虫会把自己扭动到不应该出现的地方,并坚持它能找到的第一种混凝土类型。以类似的方式,一只真正的蠕虫会在七月的任何一个下午粘在混凝土上。

为了解决你的问题,我们将从没有参数开始,然后递归地计算到10的任意限制。我们试图通过函数类型调用和模板类型参数(根据您的需求)传递模板蠕虫来调用(潜在的)函数对象。

这段代码没有考虑INVOKE语义,因为这需要大量的代码。如果您需要这一点来处理指向成员函数的指针和指向成员数据的指针,您可以为此推出自己的实现。

我可能没有涵盖所有的运算符,也可能没有正确地实现它们,但你会明白这一点的。

最后一件事:

我知道有一个陷阱返回类型不能依赖于依赖名称(成员运算符除外)

编辑:此外,调用/模板实例化需要对SFINAE友好(即没有static_assert s)。

事不宜迟,这里是您的独立解决方案(尽管您可能希望自己没有问):

#include <utility>
#include <type_traits>
namespace detail {
    //template_worm CANNOT be used in evaluated contexts
    struct template_worm {
        template<typename T>
        operator T& () const;
        template<typename T>
        operator T && () const;
        template_worm() = default;
#ifndef _MSC_VER
        // MSVC doesn't like this... because it can deduce void?
        // Whatever, we can do without it on Windows
        template<typename... T>
        template_worm(T&&...);
#endif //_MSC_VER
        template_worm operator+() const;
        template_worm operator-() const;
        template_worm operator*() const;
        template_worm operator&() const;
        template_worm operator!() const;
        template_worm operator~() const;
        template_worm operator()(...) const;
    };
#define TEMPLATE_WORM_BINARY_OPERATOR(...)                                 
                                                                           
    template<typename T>                                                   
    constexpr inline auto                                                  
    __VA_ARGS__ (template_worm, T&&) -> template_worm {                    
        return template_worm{};                                            
    }                                                                      
                                                                           
    template<typename T>                                                   
    constexpr inline auto                                                  
    __VA_ARGS__ (T&&, template_worm) -> template_worm {                    
        return template_worm{};                                            
    }                                                                      
                                                                           
    constexpr inline auto                                                  
    __VA_ARGS__ (template_worm, template_worm) -> template_worm {          
        return template_worm{};                                            
    }                                                                      
    /**/
    TEMPLATE_WORM_BINARY_OPERATOR(operator+)
    TEMPLATE_WORM_BINARY_OPERATOR(operator-)
    TEMPLATE_WORM_BINARY_OPERATOR(operator/)
    TEMPLATE_WORM_BINARY_OPERATOR(operator*)
    TEMPLATE_WORM_BINARY_OPERATOR(operator==)
    TEMPLATE_WORM_BINARY_OPERATOR(operator!=)
    TEMPLATE_WORM_BINARY_OPERATOR(operator&&)
    TEMPLATE_WORM_BINARY_OPERATOR(operator||)
    TEMPLATE_WORM_BINARY_OPERATOR(operator|)
    TEMPLATE_WORM_BINARY_OPERATOR(operator&)
    TEMPLATE_WORM_BINARY_OPERATOR(operator%)
    TEMPLATE_WORM_BINARY_OPERATOR(operator,)
    TEMPLATE_WORM_BINARY_OPERATOR(operator<<)
    TEMPLATE_WORM_BINARY_OPERATOR(operator>>)
    TEMPLATE_WORM_BINARY_OPERATOR(operator<)
    TEMPLATE_WORM_BINARY_OPERATOR(operator>)
    template<std::size_t Ignored>
    using worm_arg = template_worm const &;
    template<typename T>
    struct success {};
    struct substitution_failure {};
    template<typename F, typename... Args>
    struct invoke_test {
        template<typename T, typename... Rgs>
        auto operator()(T&& t, Rgs&&... rgs) const ->
            success<decltype(std::declval<T&&>()(std::forward<Rgs>(rgs)...))>;
        auto operator()(...) const->substitution_failure;
        static constexpr int arg_count = sizeof...(Args);
    };
    // force_template_test doesn't exist in my library
    // solution - it exists to please OP
    template<typename... Args>
    struct force_template_test {
        template<typename T>
        auto operator()(T&& t) const ->
            success<decltype(std::declval<T&&>().template operator()<Args...>())>;
        auto operator()(...) const->substitution_failure;
    };
    template<typename T, typename... Args>
    struct try_invoke {
        using test_1 = invoke_test<T, Args...>;
        using invoke_result = decltype(test_1{}(
            ::std::declval<T>(),
            ::std::declval<Args>()...
            ));
        using test_2 = force_template_test<Args...>;
        using force_template_result = decltype(test_2{}(std::declval<T>()));
        static constexpr bool value =
            !std::is_same<invoke_result, substitution_failure>::value
            || !std::is_same<force_template_result, substitution_failure>::value;
        static constexpr int arg_count = test_1::arg_count;
    };
    template<typename T>
    struct try_invoke<T, void> {
        using test = invoke_test<T>;
        using result = decltype(test{}(::std::declval<T>()));
        static constexpr bool value = !std::is_same<result, substitution_failure>::value;
        static constexpr int arg_count = test::arg_count;
    };
    template<typename U, std::size_t Max, typename = int>
    struct min_args;
    struct sentinel {};
    template<typename U, std::size_t Max>
    struct min_args<U, Max, sentinel> {
        static constexpr bool value = true;
        static constexpr int arg_count = -1;
    };
    template<typename U, std::size_t Max, std::size_t... I>
    struct min_args<U, Max, std::index_sequence<I...>> {
        using next = typename std::conditional<
            sizeof...(I)+1 <= Max,
            std::make_index_sequence<sizeof...(I)+1>,
            sentinel
        >::type;
        using result_type = std::disjunction<
            try_invoke<U, worm_arg<I>...>,
            min_args<U, Max, next>
        >;
        static constexpr bool value = result_type::value;
        static constexpr int arg_count = result_type::arg_count;
    };
    template<typename U, std::size_t Max>
    struct min_args<U, Max, void> {
        using result_type = std::disjunction<
            try_invoke<U, void>,
            min_args<U, Max, std::make_index_sequence<1>>
        >;
        static constexpr int arg_count = result_type::arg_count;
        static constexpr bool value = result_type::value;
    };
    template<typename T, std::size_t SearchLimit>
    using min_arity = std::integral_constant<int,
        min_args<T, SearchLimit, void>::arg_count>;
}
// Here you go.
template<typename T>
using is_callable = std::integral_constant<bool,
    detail::min_arity<T, 10>::value >= 0>;
// This matches OP's first example.
struct Test1 {
    template<typename T>
    T operator()() {
        return{};
    }
};
// Yup, it's "callable", at least by OP's definition...
static_assert(is_callable<Test1>::value, "");
// This matches OP's second example.
struct Test2 {
    template<typename T, typename U>
    auto operator()() -> decltype(std::declval<T>() + std::declval<U>()) {
        return{};
    }
};
// Yup, it's "callable", at least by OP's definition...
static_assert(is_callable<Test2>::value, "");
// ints aren't callable, of course
static_assert(!is_callable<int>::value, "");
int main() {}