Inline in C++ — What it has to do with the One Definition Rule.
The inline
keyword has got to be the most misunderstood keyword in C++. I remember when I first started, most online sources solely mention inline
as a way to hint the compiler to substitute logic of a function in-place of every instance of it being called; In other words, inlining.
So it is just an optimisation feature then? Well, with the advanced compilers that we have today, optimisation “hints” are often omitted. Not only because compilers have gotten really good at optimising code, humans are also often wrong, especially when it comes to optimisation.
At this point, I would not blame anyone for not looking any further into it. However, the inline
keyword plays a huge part in how your code is compiled and linked behind the scenes, especially in regards to the One Definition Rule.
I find it very unsettling that this is not discussed enough in most beginner C++ resources, but hey, that is what I am trying to do now :)
What is the One Definition Rule?
Simply put, ODR states that in the entire of a C++ program, an object or function should NOT have more than one definition. This rule is the reason something like this would throw a linker error.
// ========= beverage.h =========
#ifndef BEVERAGE_H
#define BEVERAGE_H
#include <string>
std::string GetBestBeverage(); // declare GetBestBeverage()
#endif // BEVERAGE_H
// ========= beverage.cpp =========
#include "beverage.h"
std::string GetBestBeverage() // define GetBestBeverage() here!
{
return "Pepsi!";
}
// ========= main.cpp =========
#include <iostream>
#include "beverage.h"
std::string GetBestBeverage() // define GetBestBeverage() again here!
{
return "Coke!";
}
int main()
{
std::cout << GetBestBeverage();
return 0;
}
If we try to compile and link these, we will get a linker error as expected.
$ g++ -o app main.cpp beverage.cpp
/usr/lib/gcc/x86_64-pc-cygwin/11/../../../../x86_64-pc-cygwin/bin/ld:
/tmp/cck4CARS.o:beverage.cpp:(.text+0x0):
multiple definition of `GetBestBeverage()';
/tmp/ccDeuZmj.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
But, what does this have to do with the inline
?
Inline — an ODR dismisser
Marking a function (and variables in C++17) with inline
, we effectively inform the linker that its definition can appear in multiple translation units where it is ODR-used. So, although the definition of said symbol appears in more than translation unit, the program behaves as if there is only one definition!
That said, ODR still applies here, but in the context that there should only be one definition per translation unit.
So, what gives? This allows programmers to define inline symbols in header files, because they can exist in multiple compilation units.
Therefore, we can write a program like this — defining our functions in header files.
// ========= beverage.h =========
#ifndef BEVERAGE_H
#define BEVERAGE_H
#include <string>
inline std::string GetBestBeverage() // define an inline function here!
{
return "Dr Pepper!";
}
#endif // BEVERAGE_H
// ========= beverage.cpp =========
#include <iostream>
#include "beverage.h"
void PrintFromSomewhereElse()
{
std::cout << "Here, the best beverage is "
<< GetBestBeverage() // call GetBestBeverage() from here!
<< std::endl;
}
// ========= main.cpp =========
#include <iostream>
#include "beverage.h"
void PrintFromSomewhereElse();
int main()
{
PrintFromSomewhereElse();
std::cout << "Here, the best beverage is still "
<< GetBestBeverage() // call GetBestBeverage() from here too!
<< std::endl;
return 0;
}
If we compile, link and run the following sources, we get the behaviour we expect!
$ g++ -o app main.cpp beverage.cpp
$ ./app
Here, the best beverage is Dr Pepper!
Here, the best beverage is still Dr Pepper!
If we do not have the inline
keyword when defining the function in the header file, we get a similar linker error as we did before, because there are more than one definition that exists in the entirety of the program.
// ========= beverage.h =========
#ifndef BEVERAGE_H
#define BEVERAGE_H
#include <string>
std::string GetBestBeverage() // no longer inline!
{
return "Dr Pepper!";
}
#endif // BEVERAGE_H
$ g++ -o app main.cpp beverage.cpp
/usr/lib/gcc/x86_64-pc-cygwin/11/../../../../x86_64-pc-cygwin/bin/ld:
/tmp/ccZP1VHE.o:beverage.cpp:(.text+0x0):
multiple definition of `GetBestBeverage()';
/tmp/ccLDF8Ov.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
Implicitly inline
It is noteworthy that methods or member functions that are defined within a class definition is an inline function.
// ========= beverage.h =========
#ifndef BEVERAGE_H
#define BEVERAGE_H
#include <string>
class Beverage
{
public:
// C-tors - both are inline!
Beverage(){}
Beverage(const std::string& expiry,
const std::string& brand,
const std::string& volume) :
mExpiryDate(expiry),
mBrand(brand),
mNetVolume(volume)
{}
std::string GetExpiryDate() const // inline!
{
return mExpiryDate;
}
std::string GetNetVolume() const; // inline - defined below!
inline std::string GetBrand() const; // inline - defined below!
private:
// NSDMI - C++11 feature
std::string mExpiryDate = "n/a";
std::string mBrand = "n/a";
std::string mNetVolume = "n/a";
};
inline std::string Beverage::GetNetVolume() const
{
return mNetVolume;
}
std::string Beverage::GetBrand() const
{
return mBrand;
}
#endif // BEVERAGE_H
In this case, all three methods: GetExpiryDate()
, GetNetVolume()
and GetBrand()
are all inline. Just different syntaxes.
For complete-ness, if we include this header in two source files, compile and link them, we get the expected behaviour.
// ========= beverage.cpp =========
#include "beverage.h"
#include <iostream>
void PrintBeverageFromSomewhereElse()
{
const Beverage coke("04-Jul-2023", "Coke", "330ml");
std::cout << "Beverage Brand: " << coke.GetBrand();
std::cout << "\nBeverage Expiry: " << coke.GetExpiryDate();
std::cout << "\nBeverage Volume: " << coke.GetNetVolume() << std::endl;
}
// ========= main.cpp =========
#include <iostream>
#include "beverage.h"
void PrintBeverageFromSomewhereElse();
int main()
{
PrintBeverageFromSomewhereElse();
const Beverage pepsi("01-Jan-2023", "Pepsi", "150ml");
std::cout << "Beverage Brand: " << pepsi.GetBrand();
std::cout << "\nBeverage Expiry: " << pepsi.GetExpiryDate();
std::cout << "\nBeverage Volume: " << pepsi.GetNetVolume() << std::endl;
return 0;
}
$ g++ -o app main.cpp beverage.cpp
$ ./app
Beverage Brand: Coke
Beverage Expiry: 04-Jul-2023
Beverage Volume: 330ml
Beverage Brand: Pepsi
Beverage Expiry: 01-Jan-2023
Beverage Volume: 150ml
Another important note is that template functions are also inline by default, but full template specialisations are not. This means they are subject to ODR, but also implies that they could also be inline
!
C++17’s inline variables
C++17 introduced inline
variables, which features similar semantics to that of an inline
function, just that it is for variables.
This is great as it improves readability of code such as initialisation of static
class members. We were required to initialise static
member variables somewhere in the implementation source file. But with inline
variables, we can now declare and initialise static
members in the same place!
// ========= beverage.h =========
#ifndef BEVERAGE_H
#define BEVERAGE_H
#include <string>
class Beverage
{
public:
inline static std::string sBestBeverage = "7up";
static std::string sSecondBestBeverage;
};
// equivalent to sBestBeverage, just different syntax
inline std::string Beverage::sSecondBestBeverage = "Sprite";
#endif // BEVERAGE_H
“Small” caveat — Avoiding ill-formed code
It is very important to note though, since the inline
keyword effectively dismisses linker’s checks on ODR, this allows it to choose any one of the symbol’s definitions in all the translation units it is defined in, and ignore the others.
This means it is the programmer’s responsibility to ensure that all the definitions are similar, else the program will be ill-formed!
// ========= somewhere.cpp =========
inline int Foo()
{
return 1;
}
int CallFromSomewhereElse()
{
return Foo();
}
// ========= main.cpp =========
#include <iostream>
int CallFromSomewhereElse();
inline int Foo()
{
return 42;
}
int main()
{
std::cout << CallFromSomewhereElse() << std::endl;
std::cout << Foo();
return 0;
}
The output depends on the order of compilation in the case of my compiler, but this behaviour is not guaranteed, so do not be surprised if it gives you a pint of 🍺with it!
$ g++ -o app main.cpp somewhere.cpp
$ ./app
42
42
$ g++ -o app somewhere.cpp main.cpp
$ ./app
1
1
Hopefully that clears things up on the effect of inline
in your code. I wish this was more commonly brought up in C++ resources, but a wise man once said — “be the change you wish to see in the world” :)
Feel free to leave a comment if there are any doubts, or something you would like to add!