I've been using a weird technique that helps me eliminate boiler plate C++ code. Templates. Not the C++ kind, but the web kind. Specifically, Ruby's ERB. It's like a pre-processor on steroids. You can embed Ruby code into a file, and when processed, the code is executed and replaced with the result.
It's mainly used inside of web frameworks like Ruby on Rails to generate HTML which is then sent to a web browser. However, there's no law that says it can only be used on HTML files.
Lately, I've been using ERB inside of C++ files as a code generation tool.
Here's an example where ERB templates can be useful. Look at the following Enemy class, with a message handler method.
class Enemy
{
public:
enum MessageType { APPLY_DAMAGE_TYPE, LOUD_SOUND_ALERT_TYPE };
void HandleMessage(MessageType type, void* msg);
};
void Enemy::HandleMessage(MessageType type, void* msg)
{
switch (type)
{
case DAMAGE_TYPE:
ApplyDamage((ApplyDamageMessage*)msg);
break;
case LOUD_SOUND_ALERT_TYPE:
LoudSoundAlert((LoundSoundAlertMessage*)msg);
break;
...
}
}
During development, message types are added, renamed and removed. Each time someone changes the code, four things need to be kept in sync:
- The enum value
- The case label
- The function dispatch
- The message cast.
If you instead use ERB to generate the code you can prevent those errors.
<%# This defines a Ruby array which contains all the message types %>
<% msg_types = ['apply_damage', 'loud_sound_alert'] %>
class Enemy
{
public:
enum MessageType {
<%# The message types get converted to enum values here %>
<%= msg_types.map {|type| type.upcase + "_TYPE"}.join(", ") %>
};
void HandleMessage(MessageType type, void* msg);
};
<%# Here's a template of what each case label will look like %>
<% template = <<-CODE
case LABEL:
FUNCTION((MESSAGE*)msg);
break;
CODE
# Helper function that converts variables like_this into variables LikeThis.
def humpbackize type
type.split("_").map {|word| word.capitalize}.join
end
%>
void Enemy::HandleMessage(MessageType type, void* msg)
{
switch (type)
{
<%# expand out a case label for each message type %>
<% msg_types.each do |type| %>
<%= template.gsub(/LABEL/, type.upcase + "_TYPE").
gsub(/FUNCTION/, humpbackize(type)).
gsub(/MESSAGE/, humpbackize(type) + "Message") %>
<% end %>
}
}
To add a new message just add it to the message_type array and implement the handler function. All the enum values, case labels & dispatching are all kept in sync.
You will have to integrate this into your build process, but it's not difficult. Just shell out to the erb executable.
It's not all roses, though. It's significantly harder to read then straight C++ code. Even if you know both languages well. Also, You might have to give up some syntax highlighting. Most editors can't handle mixing languages like this.
I've used this technique on personal projects and it has paid off. It's a great way to apply the DRY principle to your C++ code.