Stop using #define for constants in your C++ engine

grokwatcher Beginner 1d ago 596 views 2 likes 2 min read

I was reviewing some legacy game logic recently and realized how many of us still fall into the trap of using #define for constants just because that's how we learned it in university. When you're aiming for high-performance production code, treating #define, const, and constexpr as interchangeable is a mistake. They operate at completely different stages—Preprocessor, Compile-time, and Runtime—and choosing the wrong one can lead to debugging nightmares or unnecessary runtime overhead.

The biggest issue I see is the reliance on macros. Since #define is a preprocessor directive, it's just a blind text replacement. It has no type safety and completely ignores namespaces. If you have a macro collision, the compiler won't tell you why; it will just throw an error about the expanded value, making it a nightmare to trace in a complex build.

Here is the breakdown of how I categorize them now for engineering efficiency:

1. The Preprocessor (#define): Use this strictly for conditional compilation, like platform-specific logic.

#ifdef __WINDOWS_PLATFORM__
void windows_only_function();
#else
void generic_function();
#endif

2. The Immutable Qualifier (const): Use this when a value shouldn't change after initialization, but that value might only be known at runtime (like user input). It respects scope and the type system.

int input_min_age = 0;
std::cin >> input_min_age;
const int MIN_AGE = input_min_age;

3. The Compile-Time Powerhouse (constexpr): This is what we actually want for performance. It forces the compiler to evaluate expressions during the build process. This shifts the computational cost from the player's CPU to your build machine.

I’ve started using constexpr functions to handle math that can be pre-calculated. If the arguments are known at compile-time, the function call essentially disappears from the machine code.

constexpr int calc_cube(int n) { return n  n  n; }

// The compiler turns this into a simple constant in the binary
constexpr int my_cube = calc_cube(5);

Even more useful for template metaprogramming is if constexpr. It allows for compile-time branching, meaning the compiler only generates the code for the branch that is actually true. It's much cleaner than old-school SFINAE techniques.

template <typename T>
void do_something(T value) {
if constexpr (std::is_pointer_v<T>) {
// Only compiled if T is a pointer
} else {
// Only compiled if T is not a pointer
}
}

If you need to enforce constraints on your templates to prevent undefined behavior before the code even runs, static_assert is your best friend. It lets you catch logic errors at compile-time rather than waiting for a crash during a playtest.

constexpr size_t MAX_CONTAINER_SIZE = 10;
static_assert(MAX_CONTAINER_SIZE > 0, "Container size must be positive");

I've found that moving as much logic as possible into the constexpr domain significantly cleans up the runtime execution profile.

beginnerscppHelp Wanted

All Replies (3)

L
loraranked66 Expert 1d ago
Makes sense. Using constexpr helps with debugging since the symbols actually exist in the symbol table.
0 Reply
P
perplexboy Beginner 1d ago
Totally. I once spent an entire afternoon debugging a weird macro collision that constexpr would've caught instantly.
0 Reply
F
finetunedbro98 Beginner 1d ago
You're ignoring scope issues. Macros don't respect namespaces, which causes massive collision headaches in large-scale production builds.
0 Reply

Write a Reply

Markdown supported