- ⚙️ C++ macros can make method calls look like dynamic ones. They do this by making method names at compile time with token pasting.
- 🧩 X Macros put repeated code patterns in one place. This makes declarations, definitions, and dispatch logic easier to keep up.
- 🚫 Standard C++ cannot find function names with strings at runtime. It needs external mapping or polymorphism to do this.
- 💡 Variadic macros and token pasting let you make named function calls that take arguments. This acts like flexible dispatch.
- ⚠️ Using too many macros can make debugging hard and code difficult to read.
C++ Macros: Can You Build Method Calls Dynamically?
In C++, calling methods based on conditions that happen when the program runs is easy in interpreted languages. But in C++, it needs a different way to build things. You cannot do true dynamic dispatch without tools like polymorphism or lookup tables. However, C++ macros offer a strong option at compile time. By using preprocessor instructions, token pasting, and macro expansions, you can make flexible, low-overhead method call systems. These systems act like they have dynamic behavior and keep performance high.
Understanding C++ Macros
C++ macros act as preprocessor instructions that run before the compiler sees your code. They work only with text. This means they replace or make parts of code during the preprocessing step. This gives a lot of freedom, especially for making code, managing settings, and handling repeated patterns.
Types of Macros
-
Object-like Macros
These macros act like constant values:#define PI 3.14159 -
Function-like Macros
Accept parameters and act like functions, but are purely textual substitutions:#define SQUARE(x) ((x) * (x)) -
Variadic Macros
Accept any number of parameters using...:#define LOG(...) printf(__VA_ARGS__) -
Token-Pasting Macros
Use the##operator to join tokens:#define MAKE_VAR(name) var_##nameExpanding
MAKE_VAR(foo)becomesvar_foo.
With this base, we can build more complex patterns. These include making method names that act like dynamic ones and sending calls at compile time.
The Challenge of Dynamic Method Calls in C++
High-level scripting languages let you call functions using string names:
getattr(obj, "open")()
But C++ is a statically typed language. Functions are found when the code is built. Directly using strings to call methods is not possible on its own. It needs more structure, like polymorphism or hash maps.
This often leads to repeated if-else code that looks bad:
if (action == "create") {
do_create();
} else if (action == "delete") {
do_delete();
}
This is wordy. And it doesn't work well for many items. Luckily, C++ macros let us make this method sending code in a clean way when you know all the possible function calls during compile time.
Method Name Generation Using Macros
C++ macros let you paste tokens together. This combines names and makes new ones. The ## operator, also called the "token-pasting operator," is key to how this works.
Basic Example
#define CALL_METHOD(name) do_##name()
Given:
void do_create() { std::cout << "Creating\n"; }
void do_delete() { std::cout << "Deleting\n"; }
You can call:
CALL_METHOD(create);
CALL_METHOD(delete);
Which expands to:
do_create();
do_delete();
This removes the need to write each function call by hand when the cases are few and known beforehand.
Note: This only works when you know the function name to call at compile time. You cannot pass strings like
"create"intoCALL_METHODbecause token-pasting does not turn strings into tokens.
Real Code Example: Token Pasting in Action
Let's look at how this works in a real example:
#include <iostream>
void do_create() { std::cout << "Created\n"; }
void do_delete() { std::cout << "Deleted\n"; }
void do_update() { std::cout << "Updated\n"; }
#define CALL_METHOD(name) do_##name()
int main() {
CALL_METHOD(create); // Calls do_create()
CALL_METHOD(delete); // Calls do_delete()
CALL_METHOD(update); // Calls do_update()
}
Compile and run this, and you’ll see:
Created
Deleted
Updated
This is very good when making command senders. These have routes set at compile time that act like high-level scripting.
Going Deeper with X Macros
X Macros are a strong way to put data in one spot. You can then use this data for many parts of code. This gets rid of all repetition. They are very useful when many pieces of code (declarations, definitions, senders) share the same structure.
Step-by-Step Breakdown of X Macros
- Put the command list in one place
#define COMMAND_LIST \
X(create) \
X(delete) \
X(update)
This acts as the shared source of truth.
- Make Function Prototypes
#define X(cmd) void do_##cmd();
COMMAND_LIST
#undef X
Expands to:
void do_create();
void do_delete();
void do_update();
- Define Function Implementations
#define X(cmd) void do_##cmd() { std::cout << #cmd << " executed\n"; }
COMMAND_LIST
#undef X
Notice #cmd turns the token into a string literal. This lets you print them.
- Create the Caller Macro
#define CALL(cmd) do_##cmd()
- Use It in Main
CALL(create); // Expands to do_create();
This cuts down on copied functions and helps keep things the same. Update COMMAND_LIST one time, and all related code stays updated.
Using Variadic Macros with Function Parameters
Static function names are one piece of the puzzle. But real functions often take parameters. This is where variadic macros are very helpful.
Define a Variadic Caller
#define CALL_WITH_ARGS(func, ...) func(__VA_ARGS__)
Apply It
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!\n";
}
CALL_WITH_ARGS(greet, "Alice");
Expands to:
greet("Alice");
You can combine this with token pasting:
#define DISPATCH_FUNC(name, ...) do_##name(__VA_ARGS__)
void do_update(int id, const char* data) {
std::cout << "Updating ID " << id << " with data " << data << '\n';
}
DISPATCH_FUNC(update, 10, "NewEntry");
Gives:
do_update(10, "NewEntry");
This gives a limited, but very fast, form of function calling that acts dynamic.
Building a Macro-Based Command Dispatcher
Let's make a system that handles commands. It is easy to add to:
enum Command {
CMD_CREATE,
CMD_DELETE,
CMD_UPDATE
};
Instead of writing conditionals by hand, make the sending logic through macros.
Define the X Macro
#define COMMAND_LIST \
X(CMD_CREATE, create) \
X(CMD_DELETE, delete) \
X(CMD_UPDATE, update)
#define X(enumName, fnName) \
if (cmd == enumName) do_##fnName();
Usage
int main() {
Command cmd = CMD_UPDATE;
COMMAND_LIST
return 0;
}
This expands to:
if (cmd == CMD_CREATE) do_create();
if (cmd == CMD_DELETE) do_delete();
if (cmd == CMD_UPDATE) do_update();
This sender is easy to add to. Just add items to the COMMAND_LIST.
Real-World Applications and Benefits
Here is where using macros to make method names is useful:
1. Embedded Systems
- Macros prevent extra work at runtime, like
vtableusage. - Small, steady memory use.
- Static sending makes sure things happen on time more easily.
2. Game Development
- Imagine entity systems with 50 or more state changes.
- X Macros let you manage state handlers in one place.
- This cuts down on bugs found during compiling that come from copied code.
3. Code Generators
- Systems that make repeated standard code across functions, routes, or REST APIs.
- Change how macros expand to reduce hand changes.
4. Protocol Parsers
- Handle commands in a set binary format by using handlers that parse statically.
- Use enums made by macros with static sending.
Limitations and Caveats of C++ Macros
Before using them a lot, know about common problems.
⚠️ Debug Difficulty
- Error lines often point to expanded macro code.
- Finding problems becomes hard when expansions are many layers deep.
🚫 No Runtime Flexibility
- Macros work only at compile time.
- You cannot call
CALL_METHOD("create").
🧩 Type Safety Missing
- Macros do not check types.
- You will not get helpful compiler errors if arguments do not match.
🔍 Hard-to-Read Code
- Macros used too much look like special languages, not C++ code.
- New developers may find it hard to understand how they expand.
If you are not sure, choose templates or inline functions. These keep type checking and IDE support.
Alternatives to C++ Macros for Dynamic Behavior
C++ offers other ways to build things when macros are not enough:
1. Function Pointers
typedef void(*ActionFunc)();
std::unordered_map<std::string, ActionFunc> table = {
{"create", do_create},
{"delete", do_delete},
};
2. std::function with Maps
std::unordered_map<std::string, std::function<void()>> handlers;
handlers["update"] = []() { std::cout << "Updated\n"; };
3. Object-Oriented Polymorphism
class Command {
public:
virtual void execute() = 0;
};
class CreateCommand : public Command {
public:
void execute() override { std::cout << "Create\n"; }
};
4. Template Metaprogramming
- Gives compile-time polymorphism with type checking.
- Lets you do recursion, calculations, and specific code.
Each option works for different situations. Think about your limits.
When Should You Use C++ Macros?
Use C++ macros when:
- You want very little extra work when the program runs.
- You already know all the commands at compile time.
- You are writing code for different systems where dynamic features are limited.
- Fast performance and making code at compile time are more important than easy debugging.
Macros are like power tools. They are good and fast if you use them right. But they are dangerous if you use them wrong.
Final Thoughts
C++ macros can make method calls act dynamic. They do this through token-pasting, X macros, and variadic argument handling. These are not a replacement for true runtime features. But they give a neat, fast solution for tasks that you can predict. Use them carefully. And consider adding clear notes or simple examples to them. This will help with keeping code updated. For making code, state machines, or embedded logic, macros might be a strong tool that is often overlooked.
Ready to try macros in your next project? Try putting your handler sending inside an X Macro template. You might really like how simple and strong they are.
Citations
Stroustrup, B. (2013). The C++ Programming Language (4th ed.). Addison-Wesley.
Meyers, S. (2005). Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd ed.). Addison-Wesley.
ISO/IEC. (2017). ISO/IEC 14882:2017 (C++17 Standard).