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.
Scope
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 real memory location
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.)
Preprocessor conditions
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>;