endorph endorph - 1 year ago 51
C++ Question

Enforcing desired relationship between template function parameters

I'm working on improving an in-house messaging library, designed to send messages internally within our applications, and to external consumers. A message consists of a

enum class
) and some data (
). Each
corresponds to a particular data type (e.g.,
will always contain
). However, multiple message types could use the same struct (e.g.,
could also use

We have a class that can send messages. Our previous implementation of the message sender class defines a method for each type:

SendMessageTypeA(Foo data)
SendMessageTypeB(Bar data)
SendMessageTypeM(Foo data)

When there are lots of messages, this can result in a lot of code duplication (the method body is essentially the same, with the exception of the different parameter types).

I've implemented a new method:

template<typename structType>
void Send(MessageType msgType, const structType & messageData)

This single method can send any message, depending on the appropriate template parameter being provided. Note that the
is always known at compile time.

The problem is that this new method does not enforce the relationship between
. For example,
Send<Foo>(MessageType::TypeB, data)
will compile, even though
should contain
. The mismatch will be detected at runtime, but I'd like to make it a compile time error.

I'm not sure how to achieve this. I've considered:

  1. Declaring all the
    methods, and use them to call
    . This does reduce the duplication, but I still have to create a new method every time a message is defined.

  2. Attempting to use
    to catch the mismatch. I'm not sure how to map
    s to their desired

  3. I'm barking up the wrong tree

Answer Source

If you can lift the enum to a compile time constant, then it's possible:

template <MessageType E, class Data>
void Send(Data const& ) { ... }

We can create a class template, specialized on each enum with what that enum expects:

template <MessageType E> struct expected_type;
template <> struct expected_type<MessageType::TypeA> { using type = Foo; };
template <> struct expected_type<MessageType::TypeB> { using type = Bar; };
template <> struct expected_type<MessageType::TypeM> { using type = Foo; };

template <MessageType E>
using expected_type_t = typename expected_type<E>::type;

And then we can use that to just write that static assert:

template <MessageType E, class Data>
void Send(Data const& ) {
    static_assert(std::is_same<Data, expected_type_t<E>>{}, "!");
    // ...

Alternatively, could use that class template to set the Data type directly:

template <MessageType E>
void Send(expected_type_t<E> const& ) {