UPDATE:
Thank you Peter Sommerlad for pointing out that the non-violation of ODR was and remains the primary purpose of inline, not the optimization hint. I’m updating the post to reflect this.

Today I want to talk about the C++ keyword inline; what it used to mean prior to C++17 and what it means today. Let’s start with the historical first.

A function declared inline could be defined in a header file (its body present in the .h file), then included in multiple compilation units (.cpp files) and the linker would not complain about seeing multiple definitions of the same symbol. This was a way of stating that ODR was not being violated by the programmer. Without inline one had to provide the signature of a function in a header file, and its implementation in a source file. Alternatively inline function could be defined multiple times across multiple source files and everything would be hunky-dory as long as the definitions were identical, otherwise… undefined behavior.

inline used to also apply to standalone and member functions (class methods declared and defined inside the body of a class or struct were implicitly inline) as a hint to the compiler to inline the function call: instead of outputting assembly code that would push parameters onto the stack and jump to the function’s address the compiler would instead emit the compiled function in place, skipping the jump and stack pushes/pops. This allowed for faster running code, sometimes at the cost of the size of the executable (if the same function’s assembly was emitted in many places across the executable).
Good example of potential performance gain would be inside a tight loop making calls/jumps to a function; the overhead in each iteration of the loop could result in significant impact on performance; inline helped to mitigate that.

I mentioned earlier that inline was a hint, meaning that declaring a function as inline did not guarantee that it would be assembled in place; compilers had the ultimate say in the matter and were free to ignore inline each and every time. The workaround to this powerlessness over the mighty compiler was to instead #define the function as a macro. Preprocessor macros are evaluated and replaced with actual code prior to compilation, effectively always resulting in a function (macro) call replaced with its body in the source code.

The compiler could refuse to inline Add but it had no choice but to compile MUL in-place. Note the parenthesis around x and y in the macro; that’s there in case x and y are complex expressions that need to be fully evaluated before the final multiplication takes place. Without the parenthesis this macro call would be very problematic: MUL(1 + 2, 3 + 4); would expand to 1 + 2 * 3 + 4 which is clearly not what’s expected (due to operator precedence) at the time of the macro call.

Enter the grand inline unification!

Since C++17 the multiple definitions meaning applies equally to both functions and variables (while also being an optimization hint for functions).

If we wanted to have a global variable shared across multiple compilation units (.cpp files) we had to first declare an extern variable in a header file:

extern int x; // Inside .h file

Then define it (and provide storage for it) in a source file:

int x = 98; // Inside .cpp file

A header only workaround prior to C++17 was to use Meyer’s Singleton approach:

Starting with C++17 the same can be accomplish by simply declaring and defining the variable as inline in a header file:

inline int x = 17; // Inside .h file only

Now the header file can be included by many source files and the linker will intelligently, despite seeing multiple symbols, pick only one and disregard all others, guaranteeing the same variable at the same memory location is accessed or modified regardless of which compilation unit it happens in.

The same holds true for static member variables of a class or struct. In the past we had to do the following in a header file:

And inside a source file:

int S::x = 98; // Inside .cpp file

C++17 requires only a header file to achieve the same result:

Worth noting is that template and constexpr functions as well as constexpr variables are also implicitly inline. You can read more about all the gory details here and here.

Leave a Reply