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>
Live on Godbolt
I think it's pretty clear why this isn't usually done.