138

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;
informatik01
  • 16,038
  • 10
  • 74
  • 104
Tom Dorone
  • 1,575
  • 2
  • 9
  • 7

3 Answers3

210

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 data member 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>;
Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • `const int& h` will extend the lifetime of the temporary. If you'd used plain `int& h`, you would have been correct. – Toby Speight Feb 24 '17 at 12:35
  • 2
    @TobySpeight no, wrong on both counts. You _can't_ bind `int&` to the result, because it returns `const int&` so it won't compile. And it doesn't extend the lifetime because you're not binding the reference directly to the temporary. See http://coliru.stacked-crooked.com/a/873862de9cd8c175 – Jonathan Wakely Feb 24 '17 at 12:42
  • 1
    @TobySpeight see also http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2199.html – Jonathan Wakely Feb 24 '17 at 12:52
  • 4
    A `constexpr` variable need not occupy memory until its address (a pointer/reference) is taken; otherwise, it can be optimised away completely (and I think there might be Standardese that guarantees that). I want to emphasise this so that people don't continue using the old, inferior '`enum` hack' out of a misguided idea that a trivial `constexpr` that does not require storage will nonetheless occupy some. – underscore_d Feb 26 '17 at 22:48
  • 5
    Your "A real memory location" section is wrong: 1. You are returning by value (int), so a copy is made, the temporary isn't a problem. 2. Had you returned by reference (int&), then your `int height` would be just as a problem as the macro, since its scope is tied to the function, essentially temporary too. 3. The above comment, "const int& h will extend the lifetime of the temporary" is correct. – PoweredByRice Feb 27 '17 at 02:19
  • 6
    @underscore_d true, but that doesn't change the argument. The variable won't require storage unless there is an odr-use of it. The point is that when a real variable with storage is required, the constexpr variable does the right thing. – Jonathan Wakely Feb 27 '17 at 21:05
  • 1
    @PoweredByRice 1. the problem has nothing to do with the return value of `limit`, the problem is the return value of `std::max`. 2. yes, that's why it doesn't return a reference. 3. wrong, see the coliru link above. – Jonathan Wakely Feb 27 '17 at 21:07
  • @Jonathan Wakely I'm still not convinced the problem is the return value of `std::max` or the fact the macro is a temporary, if you assign it to `int h` it would be fine. The problem is assigning it to the reference (while juggling with temporaries). – PoweredByRice Mar 01 '17 at 04:19
  • @PoweredByRice, yes, but also if `std::max` returned by value then it would be fine. In that case the temporary's lifetime _would_ be extended. It still has nothing to do with the return type of `limit` – Jonathan Wakely Mar 01 '17 at 07:59
  • @Jonathan Wakely if `std::max` returned by value, it wouldn't have anything to do with extending the temporary's lifetime. It just makes a copy. Copying != extending lifetime. – PoweredByRice Mar 01 '17 at 14:06
  • Why wouldn't you just declare max_height as a `const int` in this case? `constexpr` seems unnecessary except for providing the example here. – pattivacek Mar 01 '17 at 16:28
  • 5
    @PoweredByRice sigh, you really don't need to explain how C++ works to me. If you have `const int& h = max(x, y);` and `max` returns by the value the lifetime of its return value is extended. Not by the return type, but by the `const int&` it is bound to. What I wrote is correct. – Jonathan Wakely Mar 01 '17 at 19:15
  • @patrickvacek yes it's an example to demonstrate the point, not to show good style. – Jonathan Wakely Mar 01 '17 at 19:15
  • For the last example, we might use: `constexpr int max_height = 720; using std::conditional_t<(max_height < 256), unsigned char, unsigned int>`. Possible example would be with stringification or name concatenation. – Jarod42 Sep 20 '18 at 14:59
  • This is literally the first time I've seen `#undef` xD I'm not sure if that's a good, bad, or sad thing. – kayleeFrye_onDeck Sep 21 '18 at 05:13
  • To corroborate the points reiterated by @JonathanWakely, a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such return statement always returns a dangling reference. This is undefined behavior. – Darnoc Eloc Jun 19 '22 at 14:14
  • Furthermore, a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference. – Darnoc Eloc Jun 19 '22 at 14:19
20

Generally speaking, you should use constexpr whenever you may, and macros only if no other solution is possible.

Rationale:

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.

Jesse Hufstetler
  • 583
  • 7
  • 13
Adrian Maire
  • 14,354
  • 9
  • 45
  • 85
  • 1
    In what case is using a macro unavoidable? – Tom Dorone Feb 22 '17 at 10:07
  • 6
    Conditional compilation using `#if` i.e. things the preprocessor is actually useful for. Defining a constant is not one of the things the preprocessor is useful for, unless that constant _must_ be a macro because it's used in preprocessor conditions using `#if`. If the constant is for use in normal C++ code (not preprocessor directives) then use a normal C++ variable, not a preprocessor macro. – Jonathan Wakely Feb 22 '17 at 10:08
  • Except using variadic macros , mostly macro use for compiler switches ,but trying to replacing current macro statements(such as conditional ,string literal switches) dealing with real code statements with constexpr is a good idea? –  Oct 10 '17 at 06:38
  • I would say compiler switches is not a good idea neither. However, I fully understand it is needed some times (also macros), especially dealing with cross-platform or embedded code. To answer your question: If you are dealing with preprocessor already, I would use macros to keep clear and intuitive what is preprocessor and what is compilation time. I would also suggest to comment heavily and make it usage as short and local as possible (avoid macros spreading around or 100 lines #if). Maybe the exception is the typical #ifndef guard ( standard for #pragma once) which is well understood. – Adrian Maire Oct 10 '17 at 07:47
11

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:

  1. Hardware and Platform-specific code sections
  2. Increased verbosity builds

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
kayleeFrye_onDeck
  • 6,648
  • 5
  • 69
  • 80
  • That second option *is* bad practise, and should be replaced by `if constexpr`s that invoke `constexpr` functions which return their respective macro values. The reason is that you still want the compiler to test that your changes aren't going to break compilation on platforms other than the one you're working on. The exception to that is, of course, when the includes and functions you're using are just completely different on the different platforms, and you simply don't have the same functions to call. – Len Dec 13 '22 at 03:52