endorph
endorph

Reputation: 193

Calling different template function specialisations based on a run-time value

This is related to a previous question in that it's part of the same system, but it's a different problem.

I'm working on an in-house messaging system, which is designed to send messages (structs) to consumers.

When a project wants to use the messaging system, it will define a set of messages (enum class), the data types (struct), and the relationship between these entities:

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; };

Note that different types of message may use the same data type.

The code for sending these messages is discussed in my previous question. There's a single templated method that can send any message, and maintains type safety using the template definitions above. It works quite nicely.

My question regards the message receiver class. There is a base class, which implements methods like these:

ReceiveMessageTypeA(const Foo & data) { /* Some default action */ };
ReceiveMessageTypeB(const Bar & data) { /* Some default action */ };
ReceiveMessageTypeM(const Foo & data) { /* Some default action */ };

It then implements a single message processing function, like this:

bool ProcessMessage(MessageType msgType, void * data) {
  switch (msgType) {
    case TypeA:
      ReceiveMessageTypeA(data);
      break;

    case TypeB:
      ReceiveMessageTypeB(data);
      break;

    // Repeat for all supported message types

    default:
      // error handling
      break;
  }
}

When a message receiver is required, this base class is extended, and the desired ReceiveMessageTypeX methods are implemented. If that particular receiver doesn't care about a message type, the corresponding function is left unimplemented, and the default from the base class is used instead.

Side note: ignore the fact that I'm passing a void * rather than the specific type. There's some more code in between to handle all that, but it's not a relevant detail.

The problem with the approach is the addition of a new message type. As well as having to define the enum, struct, and expected_type<> specialisation, the base class has to be modified to add a new ReceiveMessageTypeX default method, and the switch statement in the ProcessMessage function must be updated.

I'd like to avoid manually modifying the base class. Specifically, I'd like to use the information stored in expected_type to do the heavy lifting, and to avoid repetition.


Here's my attempted solution:

In the base class, define a method:

template <MessageType msgType>
bool Receive(expected_type<msgType>::type data) { 
    // Default implementation. Print "Message not supported", or something
}

Then, the subclasses can just implement the specialisations they care about:

template<> Receive<MessageType::TypeA>(const Foo & data) { /* Some processing */ }
// Don't care about TypeB
template<> Receive<MessageType::TypeM>(const Foo & data) { /* Some processing */ }

I think that solves part of the problem; I don't need to define new methods in the base class.

But I can't figure out how to get rid of the switch statement. I'd like to be able to do this:

bool ProcessMessage(MessageType msgType, void * data) {
  Receive<msgType>(data);
}

This won't do, of course, because templates don't work like that.

Things I've thought of:

  1. Generating the switch statement from the expected_type structure. I have no idea how to do this.
  2. Maintaining some sort of map of function pointers, and calling the desired one. The problem is that I don't know how to initialise the map without repeating the data from expected_type, which I don't want to do.
  3. Defining expected_type using a macro, and then playing preprocessor games to massage that data into a switch statement as well. This may be viable, but I try to avoid macros if possible.

So, in summary, I'd like to be able to call a different template specialisation based on a run-time value. This seems like a contradiction to me, but I'm hoping someone can point me in a useful direction. Even if that is informing me that this is not a good idea.

I can change expected_type if needed, as long as it doesn't break my Send method (see my other question).

Upvotes: 0

Views: 196

Answers (2)

Aaron McDaid
Aaron McDaid

Reputation: 27133

Your title says: "Calling different template function specialisations based on a run-time value"

That can only be done with some sort of manual switch statement, or with virtual functions.

On the one hand, it looks on the surface like you are doing object-oriented programming, but you don't yet have any virtual methods. If you find you are writing pseudo-objects everywhere, but you don't have any virtual functions, then it means you are not doing OOP. This is not a bad thing though. If you overuse OOP, then you might fail to appreciate the particular cases where it is useful and therefore it will just cause more confusion.

Simplify your code, and don't get distracted by OOP


You want the message type object to have some 'magic' associated with it, where it's MessageType controls how it is dispatched. This means you need a virtual function.

 struct message {
     virtual void Receive() = 0;
 }

 struct message_type_A : public message {
      virtual void Receive() {
          ....
      }
 }

This allows you, where appropriate, to pass these objects as message&, and to call msg.process_me()

Upvotes: 0

Andrey Turkin
Andrey Turkin

Reputation: 2499

You had right idea with expected_type and Receive templates; there's just one step left to get it all working.

First, we need to give us some means to enumerate over MessageType:

enum class MessageType {
    _FIRST = 0,
    TypeA = _FIRST,
    TypeB,
    TypeM = 100,
    _LAST
};

And then we can enumerate over MessageType at compile time and generate dispatch functions (using SFINAE to skip values not defined in expected_types):

// this overload works when expected_types has a specialization for this value of E
template<MessageType E> void processMessageHelper(MessageType msgType, void * data, typename expected_type<E>::type*) {
    if (msgType == E) Receive<E>(*(expected_type<E>::type*)data);
    else processMessageHelper<(MessageType)((int)E + 1)>(msgType, data, nullptr);
}
template<MessageType E> void processMessageHelper(MessageType msgType, void * data, bool) {
    processMessageHelper<(MessageType)((int)E + 1)>(msgType, data, nullptr);
}
template<> void processMessageHelper<MessageType::_LAST>(MessageType msgType, void * data, bool) {
    std::cout << "Unexpected message type\n";
}

void ProcessMessage(MessageType msgType, void * data) {
    processMessageHelper<MessageType::_FIRST>(msgType, data, nullptr);
}

Upvotes: 1

Related Questions