5

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!

Ross Bencina
  • 3,822
  • 1
  • 19
  • 33
  • The requirement for or-able flags creates exponential complexity - which will become apparent when you add more flag types. Is this strictly necessary? Since it happens at compile time it's surely only syntactic sugar isn't it? – Richard Hodges Dec 05 '16 at 16:14
  • @RichardHodges You may be correct about exponential complexity, but I don't understand why it would be. Care to explain? I suppose that it's syntactic sugar for an even more complicated set of explicit overloads supporting defaults for omitted flags, and requiring the user to order flags by flag type (which you don't have to do with runtime bitflags that are or-ed together). – Ross Bencina Dec 05 '16 at 16:38
  • you're or-ing types (and perhaps later, tuples of types) together. You are in effect creating your own arithmetic domain. This means you are obliged to describe the rules of the domain in terms the compiler will understand. The compiler already understands 1 + 2 = 3, but it does not know that flag1 | flag2 | flag3 = or that this is invalid if flag2 is of the same category as flag1. Two libraries spring to mind that may help - mpl and hana. – Richard Hodges Dec 05 '16 at 16:46
  • @RichardHodges: Thanks. My operator already implements those constraints for a single or-ed pair, I don't think extending it to expressions will be too hard. The main question is how to to set up `reply()` to only accept a (possibly empty) tuple of types where accepted tuples only contain types from specific flag categories (but some allowed categories may be omitted). The code in the question handles omitted flag categories, but it will ignore flags from additional flag categories rather than failing. – Ross Bencina Dec 05 '16 at 17:02
  • 1
    checking whether a tuple has a type has an answer here: http://stackoverflow.com/questions/25958259/how-do-i-find-out-if-a-tuple-contains-a-type that could be extended to check for legal sequences of types (of categories). But it's going to explode into a logic nightmare very quickly if you're not careful. – Richard Hodges Dec 05 '16 at 17:10
  • I don't think you will get any less boilerplate-y than you are at the moment, but I don't expect N-ary tuples to be very much more boilerplate-y, by analogy to std::tuple and std::pair – Caleth Dec 05 '16 at 17:16

0 Answers0