Hello!
From time to time I have the requirement to declare all messages that
an application can create at a central place. Unfortunately this usually
requires to do changes at up to 4 different locations in C++ to add a
single message. This is contradictory to the DRY rule.
Example:
#include <iostream>
#include <set>
#include <array>
#include <algorithm>
using namespace std;
struct MsgTemplate
{ int ID;
const char* TextTemplate;
};
struct Message
{ int ID;
string Text;
// Normally we should accept message arguments and
// do some formatting together with the text template.
// But let's ignore this for now.
Message(MsgTemplate tpl) : ID(tpl.ID), Text(tpl.TextTemplate) {}
};
struct MyWorker
{ // 1st location (header file)
static constexpr const MsgTemplate WARN_SOMETHING_WENT_WRONG
= { 1001, "Something went wrong" };
static constexpr const MsgTemplate ERR_SOMETHING_WENT_REALLY_WRONG
= { 1002, "Something went really wrong" };
static constexpr const MsgTemplate ERR_INVALID_MSGID
= { 2001, "Invalid message ID" };
// 2nd location (header file)
static constexpr const array<const MsgTemplate*,3> AllMessages =
{{&WARN_SOMETHING_WENT_WRONG,
&ERR_SOMETHING_WENT_REALLY_WRONG,
&ERR_INVALID_MSGID
}};
set<int> MessageFilter;
void AddFilter(int id)
{ if (!any_of(AllMessages.begin(), AllMessages.end(),
[id](const MsgTemplate* mp) -> bool { return mp->ID == id; }))
Print(Message(ERR_INVALID_MSGID));
else
MessageFilter.emplace(id);
}
void Print(const Message& msg) const
{ if (MessageFilter.find(msg.ID) == MessageFilter.end())
cout << msg.ID << '\t' << msg.Text << endl;
}
void Foo()
{ Print(Message(WARN_SOMETHING_WENT_WRONG));
throw Message(ERR_SOMETHING_WENT_REALLY_WRONG);
}
};
// 3nd location (source file)
constexpr const MsgTemplate MyWorker::WARN_SOMETHING_WENT_WRONG;
constexpr const MsgTemplate MyWorker::ERR_SOMETHING_WENT_REALLY_WRONG;
constexpr const MsgTemplate MyWorker::ERR_INVALID_MSGID;
// 4th location (source file, total count)
constexpr const array<const MsgTemplate*,3> MyWorker::AllMessages;
int main(int argc, char** argv)
{
MyWorker worker;
while (*++argv)
worker.AddFilter(atoi(*argv));
try
{ worker.Foo();
} catch (const Message& msg)
{ switch (msg.ID)
{case MyWorker::
ERR_SOMETHING_WENT_REALLY_WRONG.ID:
cout << "Caught ERR_SOMETHING_WENT_REALLY_WRONG\n";
break;
default:
worker.Print(msg);
}
return msg.ID;
}
return 0;
}
The above example demonstrates the main requirements:
- Each message template has a (unique) ID.
- Each message template has a symbol name in the code.
- There is a list of all possible messages.
- Messages could be dispatched by ID with a switch statement.
Until now I always ended up by some Perl script that created the code
files from a common XML source or something like that. Now I try to
figure out what is possible with C++11. But, as you can see, I did not
get that far.
I am also confused because I need to define the static constexpr members
additionally in the source code to avoid linker errors. I would have
expected that the constexpr MsgTemplate fields would expand to constants
at the place where the messages are constructed by the inline
constructor, as it obviously works at the place switch(msg.ID).
Any ideas for a better design? The above example is awful.
I already tried to define only the global array with all message
templates and define constexpr symbol names to the array elements. This
removes some redundancy. But it is error prone to assign the symbol
names because the array elements can only be accessed by index.
Marcel