Reputation: 2399
Suppose I have a class
enum CallbackType
{
SYNC,
ASYNC
}
template<CallbackType CB = SYNC, typename... Args>
class Callback
{
}
I would like to be able to optionally specificy the callback type while still being ablet to have variadic template argument. Now I understand that the compiler can't tell them apart, but maybe there is some way to handle the specific case where the first template argument is of CallbackType ?
Callback<int int> //Should be Callback<SYNC, int, int>
Callback<ASYNC, int, int> //Should be Callback<ASYNC, int, int>
Upvotes: 8
Views: 2423
Reputation: 25277
You can get a syntax very close to your original syntax with a bit of metaprogramming. You'd define your CallbackType
and a CallbackImpl
:
enum CallbackType
{
SYNC,
ASYNC,
};
template<CallbackType CB, typename... Args>
class CallbackImpl
{
};
Then do some things to get "default arguments":
// We need to treat the CallbackType argument as a type, not as a value.
// Thus, we need to wrap it in a type.
template <CallbackType cb>
using CallbackT = std::integral_constant<CallbackType, cb>;
// We need to be able to detect if the first type passed in is this CallbackT
template <typename T>
struct is_callback_type
: std::false_type
{};
template <CallbackType cb>
struct is_callback_type<CallbackT<cb>>
: std::true_type
{};
template <typename T>
using is_callback_type_t = typename is_callback_type<T>::type;
// Here we do the work. This is the base case, where the first arg
// is not a CallbackT. Note that this works for an empty Args as well
template <typename AlwaysVoid, typename... Args>
struct construct_callback_impl
{
using type = CallbackImpl<SYNC, Args...>;
};
// If the Args list is of at least size 1,
template <typename CallbackType, typename... Args>
struct construct_callback_impl<
// Use this specialization only if the first type is our CallbackT
typename std::enable_if<is_callback_type_t<CallbackType>::value>::type,
CallbackType,
Args...>
{
// Forward the specified CallbackType on to the CallbackImpl
using type = CallbackImpl<CallbackType::value, Args...>;
};
// Wrap this utility into a nicer calling syntax
template <typename... Args>
using Callback = typename construct_callback_impl<void, Args...>::type;
Then, it can be used:
Callback<int, int> // type is CallbackImpl<SYNC, int, int>
Callback<CallbackT<SYNC>, int, int> // type is CallbackImpl<SYNC, int, int>
Callback<CallbackT<ASYNC>, int, int> // type is CallbackImpl<ASYNC, int, int>
Callback<> // type is CallbackImpl<SYNC>
I think it's pretty clear why this isn't usually done.
Upvotes: 3
Reputation: 118292
There are two aspects of C++, when it comes to variadic templates, that are in conflict with each other in your case:
Defaulted template parameters should not precede non-defaulted template parameters.
Variadic template parameters should not precede non-variadic template parameters.
It is certainly possible, in many situations, to correctly declare, and use, templates whose parameters don't follow these rules, but those situations are not important for the purpose of this question. In your case, what it comes down to is that both of your template parameters want to be the last parameter in their template, for their own individual reasons. That's the problem, in a nutshell.
The easiest way to resolve this conflict is to use an inner template:
template<CallbackType CB = ASYNC>
class CallbackClass {
public:
template<typename... Args> class Callback
{
}
};
Then, your two examples become:
CallbackClass<>::Callback<int, int>
and
CallbackClass<ASYNC>::Callback<int, int>
You'll end up with longer class names, of course. But that's what typedef
and using
is for. For example:
template<typename ...Args>
using DefaultCallback=CallbackClass<>::Callback<Args...>;
then use
DefaultCallback<int, int>
Upvotes: 10