C++ / a working model

10 / 163   ·   C++11   ·   8 min

Macros and inline: Text Substitution Is Not a Function Call

Keep this sentence

Function-like macros replace tokens during preprocessing and do not provide parameter types, ordinary scope, or a single-evaluation guarantee. An inline function has full function semantics; the specifier mainly concerns definition rules across translation units and does not force machine-code inlining. For constants and computation, prefer constexpr or templates.

In this lesson
  1. The expression exists only after expansion
  2. Functions keep types and call boundaries
  3. The important promise of inline is a definition rule
  4. Example
  5. Exercise

The expression exists only after expansion

The macro #define BAD_DOUBLE(x) x + x receives preprocessing tokens. 3 * BAD_DOUBLE(1 + 2) expands to 3 * 1 + 2 + 1 + 2, which yields 8 rather than the expected 18. Parentheses around the parameter and around the whole replacement can fix this precedence problem, but they cannot fix repeated evaluation.

For example, using a macro that mentions its parameter twice on an increment expression can change how many times the side effect runs, or even cause undefined behavior, so the example uses side-effect-free literals to show the mistake. Macros also do not obey C++ namespace scope; a coincidentally matching member or library-function token can still be replaced by the preprocessor. The substitution happens before parsing, so there is no parameter type, no overload resolution, and no call boundary that evaluates the argument once. Treat a function-like macro as token pasting with a function-shaped spelling, not as a cheaper function.

Functions keep types and call boundaries

An ordinary function or function template receives arguments under the language rules; type checking and overload resolution happen at the call site. The doubling function in the example takes an int by value, so in the call twice(++n) the increment happens once and the function body reuses the parameter value instead of pasting the caller's expression twice.

constexpr allows a call that meets the requirements to participate in constant evaluation; it does not require every call to happen at compile time. It is a good replacement for expression constants and small pure-computation macros; templates are appropriate when you need to keep type generalization. Conditional compilation, stringizing, and token pasting remain different jobs that the preprocessor is still good at. Once the work is a typed computation, a function gives you a name, a signature, and a single evaluation of each argument. Keep macros for the tasks that truly need tokens, not for arithmetic that a constexpr function or a template already expresses.

The important promise of inline is a definition rule

inline does not mean “force the call overhead to disappear.” A compiler may refuse to inline a function marked inline, and it may inline a function that is not marked inline. For ordinary header code, inline allows the same function definition to appear in multiple translation units under the One Definition Rule while still naming a single entity.

Multiple definitions must satisfy the ODR conditions; different source files must not use different macros to turn the function body into different implementations. A constexpr function is implicitly inline; a member function defined in a class in an ordinary header scenario is also implicitly inline, and named modules have separate rules. Blaming a performance issue on whether this keyword was written usually ignores the real optimization context. Use inline when you need a function to be defined in a header and still be one function; measure inlining as an optimizer result, not as a spelling of the specifier.

Pitfalls

  • Parentheses around a whole macro argument only fix how tokens bind; they do not guarantee that the argument is evaluated once.
  • inline does not allow different translation units to provide mutually inconsistent function definitions; such an ODR violation may have no reliable diagnostic.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>

#define BAD_DOUBLE(x) x + x

constexpr int twice(int value) { return value + value; }

int main() {
    const int expanded = 3 * BAD_DOUBLE(1 + 2);
    const int called = 3 * twice(1 + 2);
    int n = 2;
    const int one_evaluation = twice(++n);
    assert(expanded == 8);
    assert(called == 18);
    assert(n == 3 && one_evaluation == 6);
    std::cout << expanded << ' ' << called << ' ' << n << '\n';
}

#undef BAD_DOUBLE

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-macros-inline.cpp -o example && ./example

Expected result

8 18 3

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Replace #define SQUARE(x) ((x) * (x)) with a C++11 function that keeps single evaluation of the argument, and state what arithmetic assumption remains.

Show a reference answer

You can write constexpr int square(int x) { return x * x; }. square(++n) first passes the result of one increment as a value, then uses that value twice. Integer multiplication can still overflow; the interface must restrict the input range or switch to a checked algorithm. Replacing the macro removes repeated evaluation; it does not automatically remove numeric overflow.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog