Where should I prefer using macros and where should I prefer constexpr? Aren't they basically the same?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Where should I prefer using macros and where should I prefer constexpr? Aren't they basically the same?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Aren't they basically the same?
No. Absolutely not. Not even close.
Apart from the fact your macro is an int
and your constexpr unsigned
is an unsigned
, there are important differences and macros only have one advantage.
A macro is defined by the preprocessor and is simply substituted into the code every time it occurs. The preprocessor is dumb and doesn't understand C++ syntax or semantics. Macros ignore scopes such as namespaces, classes or function blocks, so you can't use a name for anything else in a source file. That's not true for a constant defined as a proper C++ variable:
#define MAX_HEIGHT 720
constexpr int max_height = 720;
class Window {
// ...
int max_height;
};
It's fine to have a member variable called max_height
because it's a class member and so has a different scope, and is distinct from the one at namespace scope. If you tried to reuse the name MAX_HEIGHT
for the member then the preprocessor would change it to this nonsense that wouldn't compile:
class Window {
// ...
int 720;
};
This is why you have to give macros UGLY_SHOUTY_NAMES
to ensure they stand out and you can be careful about naming them to avoid clashes. If you don't use macros unnecessarily you don't have to worry about that (and don't have to read SHOUTY_NAMES
).
If you just want a constant inside a function you can't do that with a macro, because the preprocessor doesn't know what a function is or what it means to be inside it. To limit a macro to only a certain part of a file you need to #undef
it again:
int limit(int height) {
#define MAX_HEIGHT 720
return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}
Compare to the far more sensible:
int limit(int height) {
constexpr int max_height = 720;
return std::max(height, max_height);
}
Why would you prefer the macro one?
A constexpr variable is a variable so it actually exists in the program and you can do normal C++ things like take its address and bind a reference to it.
This code has undefined behaviour:
#define MAX_HEIGHT 720
int limit(int height) {
const int& h = std::max(height, MAX_HEIGHT);
// ...
return h;
}
The problem is that MAX_HEIGHT
isn't a variable, so for the call to std::max
a temporary int
must be created by the compiler. The reference that is returned by std::max
might then refer to that temporary, which doesn't exist after the end of that statement, so return h
accesses invalid memory.
That problem simply doesn't exist with a proper variable, because it has a fixed location in memory that doesn't go away:
int limit(int height) {
constexpr int max_height = 720;
const int& h = std::max(height, max_height);
// ...
return h;
}
(In practice you'd probably declare int h
not const int& h
but the problem can arise in more subtle contexts.)
The only time to prefer a macro is when you need its value to be understood by the preprocessor, for use in #if
conditions, e.g.
#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif
You couldn't use a variable here, because the preprocessor doesn't understand how to refer to variables by name. It only understands basic very basic things like macro expansion and directives beginning with #
(like #include
and #define
and #if
).
If you want a constant that can be understood by the preprocessor then you should use the preprocessor to define it. If you want a constant for normal C++ code, use normal C++ code.
The example above is just to demonstrate a preprocessor condition, but even that code could avoid using the preprocessor:
using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
Generally speaking, you should use constexpr
whenever you may, and macros only if no other solution is possible.
Macros are a simple replacement in the code, and for this reason, they often generate conflicts (e.g. windows.h max
macro vs std::max
). Additionally, a macro which works may easily be used in a different way which can then trigger strange compilation errors. (e.g. Q_PROPERTY
used on structure members)
Due to all those uncertainties, it is good code style to avoid macros, exactly like you'd usually avoid gotos.
constexpr
is semantically defined, and thus typically generates far less issues.
Great answer by Jonathon Wakely. I'd also advise you to take a look at jogojapan's answer as to what the difference is between const
and constexpr
before you even go about considering the usage of macros.
Macros are dumb, but in a good way. Ostensibly nowadays they're a build-aid for when you want very specific parts of your code to only be compiled in the presence of certain build parameters getting "defined". Usually, all that means is taking your macro name, or better yet, let's call it a Trigger
, and adding things like, /D:Trigger
, -DTrigger
, etc. to the build tools being used.
While there's many different uses for macros, these are the two I see most often that aren't bad/out-dated practices:
So while you can in the OP's case accomplish the same goal of defining an int with constexpr
or a MACRO
, it's unlikely the two will have overlap when using modern conventions. Here's some common macro-usage that hasn't been phased out, yet.
#if defined VERBOSE || defined DEBUG || defined MSG_ALL
// Verbose message-handling code here
#endif
As another example for macro-usage, let's say you have some upcoming hardware to release, or maybe a specific generation of it that has some tricky workarounds that the others don't require. We'll define this macro as GEN_3_HW
.
#if defined GEN_3_HW && defined _WIN64
// Windows-only special handling for 64-bit upcoming hardware
#elif defined GEN_3_HW && defined __APPLE__
// Special handling for macs on the new hardware
#elif !defined _WIN32 && !defined __linux__ && !defined __APPLE__ && !defined __ANDROID__ && !defined __unix__ && !defined __arm__
// Greetings, Outlander! ;)
#else
// Generic handling
#endif