Search code examples
c++c++11templatessfinae

SFINAE does not disable one of the functions


I have a packaged_task class which wraps a Callable and has invoke() functions, one if Callable does not take any arguments, and a second one when it does.

I get errors accessing typename traits::template arg<0>::type for the second one, even though it should be disabled by enable_if SFINAE.

Any idea why I am getting this behavior?

#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;
}

Solution

  • The compiler is allowed to check the validity of templates, even before instantiation. To delay this, you have to make your code dependent on template parameters. The problem is here:

    // 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) { }
    

    When the packaged_task class template is instantiated, traits becomes known, and this means that traits::arg<0> can also be diagnosed. Validity checks are then performed before you ever call the invoke function, and your code fails to compile.

    Dirty Solution

    To fix this, you would have to make arg<0> somehow depend on 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) { }
    

    By using multiplication with zero in 0 * n_args, the expression becomes dependent on the template parameter n_args, and the validity check is delayed until instantiation of the function template. The result remains zero.

    However, this solution puts faith into the compiler not diagnosing 0 * n_args as zero. The code is arguably still ill-formed, no diagnostic required. It works with every major compiler, but isn't very idiomatic, and probably incorrect.

    Clean Solution

    // 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 ...
    };
    

    This solution may seem lengthy at first, but you can at least remove packaged_task_impl_base if you feel like you can just copy and paste its contents into each partial specialization of packaged_task_impl.

    It also has the advantage that invoke() is a regular member function, and last but not least, it's definitely not relying on tricks where your program is still arguably ill-formed.