SFINAE 不会禁用其中一项功能

SFINAE does not disable one of the functions

提问人:Quest 提问时间:8/14/2023 最后编辑:Jan SchultkeQuest 更新时间:8/14/2023 访问量:87

问:

我有一个包装 a 并具有函数的类,一个如果不接受任何参数,另一个则使用。packaged_taskCallableinvoke()Callable

我在访问第二个时遇到错误,即使它应该被 SFINAE 禁用。typename traits::template arg<0>::typeenable_if

知道我为什么会有这种行为吗?

#include <functional>
#include <type_traits>
namespace detail
{
    template<typename T, typename Enabler = void>
    struct function_traits;

    template <typename T>
    struct function_traits<T, typename std::enable_if<std::is_class<T>{}>::type> : public function_traits<decltype(&T::operator())>
    {
    };

    template <typename ClassType, typename ReturnType, typename... Args>
    struct function_traits<ReturnType(ClassType::*)(Args...) const>
    {
        using result_type = ReturnType;
        static constexpr auto n_args = sizeof...(Args);

        template<std::size_t I>
        struct arg { using type = typename std::tuple_element<I, std::tuple<Args...>>::type; };

        template<std::size_t I>
        using arg_t = typename arg<I>::type;
    };

    template <typename ReturnType, typename... Args>
    struct function_traits<ReturnType(Args...)>
    {
        using result_type = ReturnType;
        static constexpr auto n_args = sizeof...(Args);

        template<std::size_t I>
        struct arg { using type = typename std::tuple_element<I, std::tuple<Args...>>::type; };

        template<std::size_t I>
        using arg_t = typename arg<I>::type;
    };
}

template<typename Callable>
class packaged_task
{
public:
    using traits = detail::function_traits<Callable>;

    template<std::size_t n_args = traits::n_args>
    typename std::enable_if<n_args == 0>::type
    invoke()
    {
    }

    template<std::size_t n_args = traits::n_args>
    typename std::enable_if<n_args == 1>::type
    invoke(typename traits::template arg<0>::type val)
    {
    }
};
int main()
{
    packaged_task<void()> task;
}
C++ C++11 模板 sfinae

评论

1赞 Quest 8/14/2023
invoke_helper或task_result的定义无关紧要。更新了代码以满足 MVP
1赞 Pepijn Kramer 8/14/2023
我假设您正在将其作为练习来做,无论如何,一些旁注:我会将大量工作留给 lambda 表达式(而不是使用类成员函数专用化),因此您也可以获得 lambda 捕获的所有好处,并且最终的模板代码要容易得多(然后 fn 将作为接受您的 lambda 的模板参数fn_t实例)。(当然已经有 std::p ackaged_task)result_typedecltype(fn())
0赞 Yakk - Adam Nevraumont 8/15/2023
我发现如果您可以访问模板中的类型,例如 .然后你调用“SFINAE”的东西变得微不足道 - 它只是.你并没有那么多地与语言作斗争。packaged_tasktemplate<class R, class...Args>class packaged_task<R(Args...)>R invoke(Args...)

答:

3赞 Jan Schultke 8/14/2023 #1

允许编译器检查模板的有效性,甚至在实例化之前。要延迟此操作,必须使代码依赖于模板参数。问题出在这里:

// OK, delay validity check by making n_args depend on traits::n_args,
//     which depends on the template parameter of the enclosing template
template<std::size_t n_args = traits::n_args>
// OK, this will do SFINAE based on the n_args template parameter to this function
typename std::enable_if<n_args == 1>::type
// ILL-FORMED!!, validity of arg<0> is checked BEFORE instantiation of this
//               member function template
invoke(typename traits::template arg<0>::type val) { }

当类模板实例化时,就会变得已知,这意味着也可以进行诊断。然后,在调用函数之前执行有效性检查,并且代码无法编译。packaged_tasktraitstraits::arg<0>invoke

肮脏的解决方案

要解决此问题,您必须以某种方式依赖:arg<0>n_args

template<std::size_t n_args = traits::n_args>
typename std::enable_if<n_args == 1>::type
invoke(typename traits::template arg<0 * n_args>::type val) { }

通过使用 中为零的乘法,表达式变得依赖于模板参数,并且有效性检查被延迟到函数模板的实例化。结果仍为零。0 * n_argsn_args

但是,此解决方案将信心置于编译器中,而不是诊断为零。可以说,该代码的格式仍然不正确,不需要诊断。它适用于每个主要的编译器,但不是很惯用,而且可能不正确。0 * n_args

清洁解决方案

// common base class for 0 args and 1 args which gets the traits
// of the function
template<typename Callable>
struct packaged_task_impl_base {
    using traits = detail::function_traits<Callable>;
};

// partially specialized class template
template<typename Callable, std::size_t n_args>
struct packaged_task_impl;

// partial specialization for 0 args
template<typename Callable>
struct packaged_task_impl<Callable, 0>
  : packaged_task_impl_base<Callable>
{
    void invoke() { /* ... */ }
};

// partial specialization for 1 arg
template<typename Callable>
struct packaged_task_impl<Callable, 1>
  : packaged_task_impl_base<Callable>
{
    void invoke() { /* ... */ }
};

// wrapper which doesn't expose n_args in its template-head
template<typename Callable>
struct packaged_task
  : private packaged_task_impl<Callable, detail::function_traits<Callable>::n_args>
{
    using traits = typename packaged_task_impl<Callable,
        detail::function_traits<Callable>::n_args>::traits;
    // other stuff which doesn't depend on n_args here ...
};

这个解决方案乍一看可能看起来很长,但如果你觉得你可以将其内容复制并粘贴到每个部分专业化中,你至少可以删除。packaged_task_impl_basepackaged_task_impl

它还具有常规成员函数的优点,最后但并非最不重要的一点是,它绝对不会依赖于程序仍然可以说是格式错误的技巧。invoke()

评论

0赞 Quest 8/14/2023
这实际上很酷,我总是认为一旦你在成员函数模板中有一个依赖类型,整个事情就会延迟。
0赞 Quest 8/14/2023
我真的很喜欢你的清洁解决方案。最初,在知道问题出在哪里之前,我曾想过将其分成部分专业化,但想知道它是否足够干净,易于维护,但这看起来很整洁。
0赞 Yakk - Adam Nevraumont 8/15/2023
我自己会把 和 放入一个基于 CRTP 的基类中 - 的主体可以得到完整的类型并使用它。invoke()invoke(one arg)invokepackaged_task