Constexpr vs macros

2020-02-07 16:58发布

问题:

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;

回答1:

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>;


回答2:

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.



回答3:

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