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