I would like to use tag dispatch to implement a kind of "mode select" parameter to select among multiple implementations of a function. This is easy when there is only one function, and one axis of variation. However, I would like to implement something more flexible supporting:
- Multiple axis of variation
- Use
operator|
to combine one mode from each axis - Different combinations of an intersecting set of axis of variations for different functions
I will give a working example below. Here is a quick preview of what I mean:
A function send()
has a SendMode
selector, which is one of { default_dispatch, enqueue, direct_deliver
}. You can write something like:
send("text"); // uses the default SendMode tag
send("text", default_dispatch);
send("text", enqueue);
send("text", direct_deliver);
send
has one axis of variation: the SendMode
;
A function reply()
can take a SendMode
, but also a ReplyCompletionAction
selector, which is one of { pre_complete, complete, post_complete }
. You can write things like:
reply("text"); // use default SendMode and ReplyCompletionAction
reply("text", default_dispatch); // use default reply ReplyCompletionAction
reply("text", pre_complete); // use default SendMode
reply("text", enqueue | post_complete); // combine both with |
reply
has two axes of variation: SendMode
and ReplyCompletionAction
. At most, one of each can be supplied, and combined with |
.
For full context, I will give my current solution below. But first, here are my questions:
- Can anyone suggest a better way to do this while retaining compile-time selection of all function variants? Better meaning: type-safe, less boiler-plate (all of sec 2. below is boiler plate), better error messages.
- Can you recommend any related techniques?
- The current method only supports combining two variation axis. It would be good to combine arbitrary axis into a tuple.
- Update: A large part of the question relates to the implementation of
reply()
below. How best can it be made both succinct, and express exactly the types that it accepts.
Thanks!
The code below compiles and runs. I have provided the full listing in a gist here: https://gist.github.com/RossBencina/01148cea6125971b0fbea4b4752418b2
Current Solution: Part 1 - send() function
This is the easy part.
First, a type-level enum helper
template<typename EnumClass, EnumClass X>
struct EnumValue {
using enum_class_type = EnumClass;
static constexpr enum_class_type value = X;
};
Tag-based flag parameters:
enum class SendMode {
default_dispatch,
enqueue,
direct_deliver
};
constexpr EnumValue<SendMode, SendMode::default_dispatch> default_dispatch = {};
constexpr EnumValue<SendMode, SendMode::enqueue> enqueue = {};
constexpr EnumValue<SendMode, SendMode::direct_deliver> direct_deliver = {};
send()
uses tag-dispatch to specify send mode
void send(const char *s, decltype(default_dispatch)={})
{
std::cout << "send: default_dispatch " << s << "\n";
// specialized code for particular send mode goes here
}
void send(const char *s, decltype(enqueue))
{
std::cout << "send: enqueue " << s << "\n";
// specialized code for particular send mode goes here
}
void send(const char *s, decltype(direct_deliver))
{
std::cout << "send: direct_deliver " << s << "\n";
// specialized code for particular send mode goes here
}
Excercise the code
void TEST_send()
{
send("0");
send("1", default_dispatch);
send("2", enqueue);
send("3", direct_deliver);
}
Current Solution: Part 2 - mechanism for combing flags
We allow |
to combine the two flags into an EnumValuePair
This is a little limited (ideally we'd support combining more flags into tuples somehow).
template<typename EnumClass1, EnumClass1 X1, typename EnumClass2, EnumClass2 X2>
struct EnumValuePair {
static_assert(not std::is_same<EnumClass1, EnumClass2>::value, "Can't combine two enums of the same type.");
using first_type = EnumValue<EnumClass1, X1>;
using second_type = EnumValue<EnumClass2, X2>;
};
template<typename EnumClass1, EnumClass1 X1, typename EnumClass2, EnumClass2 X2>
constexpr auto operator|(EnumValue<EnumClass1, X1>, EnumValue<EnumClass2, X2>)
{
return EnumValuePair<EnumClass1, X1, EnumClass2, X2> {};
}
Extraction mechanism. Allows us to extract individual flags from an expression that might be a single flag, or multiple flags or-ed together
Given type T
, extract an EnumValueType
whose enum_class_type is EnumClass
or use Default
if no match is found. At the moment we match
individual EnumValue
instances, and EnumValuePairs
(in future we could
do the look up in tuples for example).
// Base case: `T` doesn't match `EnumClass`. use `Default`
template <typename EnumClass, typename T, typename Default>
struct get_enum_value {
using type = Default;
};
// specializations...
// Single value case, exact mach: T is an EnumValue<EnumClass, X>
template<typename EnumClass, EnumClass X, typename Default>
struct get_enum_value<EnumClass, EnumValue<EnumClass, X>, Default> {
using type = EnumValue<EnumClass, X>;
};
// Pair of values. first matches
template<typename EnumClass1, EnumClass1 X1, typename EnumClass2, EnumClass2 X2, typename Default>
struct get_enum_value<EnumClass1, EnumValuePair<EnumClass1, X1, EnumClass2, X2>, Default> {
using type = EnumValue<EnumClass1, X1>;
};
// second matches
template<typename EnumClass1, EnumClass1 X1, typename EnumClass2, EnumClass2 X2, typename Default>
struct get_enum_value<EnumClass2, EnumValuePair<EnumClass1, X1, EnumClass2, X2>, Default> {
using type = EnumValue<EnumClass2, X2>;
};
// Use `monostate` to represent the case where no flags are supplied.
struct monostate {};
Current Solution: Part 3 - reply() function
This is similar to the implementation of send()
except that we need to provide
a way to extract two different modes from the single flag parameter.
enum class ReplyCompletionAction {
pre_complete,
complete,
post_complete
};
constexpr EnumValue<ReplyCompletionAction, ReplyCompletionAction::pre_complete> pre_complete = {};
constexpr EnumValue<ReplyCompletionAction, ReplyCompletionAction::complete> complete = {};
constexpr EnumValue<ReplyCompletionAction, ReplyCompletionAction::post_complete> post_complete = {};
Implementation of the different variants of reply()
.
The structure of the example is that reply()
invokes send()
. We can
capture send()
's tag via a template parameter and pass it through.
template<typename SendMode_>
void reply_(const char *s, SendMode_, decltype(pre_complete))
{
std::cout << "reply_: pre_complete " << s << "\n";
// specialized code for particular reply mode goes here
send(s, SendMode_{});
}
template<typename SendMode_>
void reply_(const char *s, SendMode_, decltype(complete))
{
std::cout << "reply_: complete " << s << "\n";
// specialized code for particular reply mode goes here
send(s, SendMode_{});
}
template<typename SendMode_>
void reply_(const char *s, SendMode_, decltype(post_complete))
{
std::cout << "reply_: post_complete " << s << "\n";
// specialized code for particular reply mode goes here
send(s, SendMode_{});
}
Public reply()
function:
// BUG: Any type at all could be passed as `ReplyFlags_` and it would be valid
// ideally we'd restrict ReplyFlags_ to only containing (at most) `SendMode` and `ReplyCompletionAction`
template<typename ReplyFlags_=monostate>
void reply(const char *s, ReplyFlags_={})
{
reply_(s,
typename get_enum_value<SendMode, ReplyFlags_, decltype(default_dispatch)>::type {},
typename get_enum_value<ReplyCompletionAction, ReplyFlags_, decltype(complete)>::type {});
}
Test:
void TEST_reply()
{
reply("0"); // use default send mode and reply mode
reply("1", default_dispatch); // use default reply mode...
reply("2", enqueue);
reply("3", direct_deliver);
reply("4", pre_complete); // use default send mode...
reply("5", post_complete);
reply("6", complete);
reply("7", enqueue | post_complete); // send mode and reply mode
reply("8", direct_deliver | complete);
//reply("9", direct_deliver | enqueue); // correctly fails to compile, two send modes
//reply("10", post_complete | complete); // correctly fails to compile, two reply modes
}
int main()
{
TEST_send();
TEST_reply();
}
Thanks!