Inline variables

  • Introduction

  • Description and examples

    • Evolution of the term inline

    • One definition rule and how to break it?

    • Global constants

      • A little about linking

      • Global constants as internally bound variables

      • Global constants with external binding

      • Global constants as inline variables

    • Initializing static class fields

    • Remarks

  • Links

Introduction

Description and examples

Evolution of the term inline

The original purpose of the inline keyword was to serve as an indicator to the optimizer that inline function substitution is preferable to a function call, that is, instead of executing a CPU instruction to transfer control to the function body, a copy of the function body is executed without generating a call. This inline expansion is based on the idea that making a function call is relatively expensive: it requires jumping to a new subroutine, passing the arguments to the function, and copying the return values. Inline expansion suppresses function invocation by copying function statements directly into the body of the caller.
The consequences of such optimization in a program can be very difficult to foresee. In addition to reducing function call overhead, inline expansion allows for a wide variety of additional optimizations that would otherwise be very difficult to perform between function calls. Keep in mind, however, that inline expansion creates a copy of the function body for each call. As a consequence, in addition to the obvious increase in program size, duplication of instructions makes the program cache-unfriendly.
Inline expansion can significantly improve performance, but this is not certain. Performance should be measured, not assumed.
Because programmers can rarely manage optimizations well, this responsibility has been removed from them, and modern compilers decide for themselves when to use inline expansion and when not to.

Today is the key word inline has little to do with inline expansion. This optimization is optional on the compiler, compilers are free to use inline replacement for any function that is not labeled inline, and can generate function calls for any function marked inline
However, the key word inline also touches on linking issues. You can read more about this. here… The compiler’s optimization behavior does not change the rules for multiple definitions. Today inline – this is about one definition rule, not about inline expansion optimization. And since the meaning of the keyword inline for functions came to mean “multiple definitions allowed” rather than “preferably inline”, this meaning was extended to variables.

One definition rule and how to break it?

Functions and variables declared inline can be defined multiple times in a program.
This is, in fact, an exception to rules of one definition… The ODR says that you can only define functions, variables, classes, enumeration, etc. once.
ODR must be performed not only at the translation unit level, but also at the program level.

Built-in functions and variables are an exception to the rule of one definition: they can be defined multiple times in a program (multiple times in a program, but only once in one translation unit).

Let’s look at a couple of examples: using inline when declaring and initializing global constants, and using inline when declaring and initializing static fields of a class.

Global constants

Often times, certain symbolic constants can be used in different parts of the program (not just in one place). These can be physical or mathematical constants that do not change (for example, Pi or Avogadro’s number), or application-specific values ​​(for example, coefficients of friction or gravity). Instead of redefining these constants in every file that needs them, it is better to declare them once in one place and use them wherever needed. Then if you ever need to change them, you only need to change them in one place.

A little about linking

The translation unit includes the implementation file (.c / .cpp) and all its header files (.h / .hpp).
If an object or function has an internal linking inside a translation unit, then this symbol is visible to the linker only inside this translation unit. If the object or function has an external binding, then the linker will be able to see it when processing other translation units. Using the static keyword in the global namespace gives the symbol an internal linkage. The extern keyword gives you external linking.
The compiler gives symbols the following bindings by default:

  • Non-const global variables – external binding;

  • Const global variables – internal binding;

  • Functions are external binding.

More details …

Global constants as internally bound variables

One way to do it:

  1. Create a header file to store these constants.

  2. Within this header file, define a namespace.

  3. Add all your constants to the namespace (make sure they are constexpr).

  4. #include your header file wherever needed.

For example:

// constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
 
// define your own namespace to hold constants
namespace constants
{
    // constants have internal linkage by default
    constexpr double pi { 3.14159 };
    constexpr double avogadro { 6.0221413e23 };
    constexpr double my_gravity { 9.2 }; // m/s^2 -- gravity is light on this planet
    // ... other related constants
}
#endif

And use your constants:

// main.cpp
#include "constants.h" // include a copy of each constant in this file
 
#include <iostream>
 
int main()
{
    std::cout << "Enter a radius: ";
    int radius{};
    std::cin >> radius;
 
    std::cout << "The circumference is: " << 2.0 * radius * constants::pi << 'n';
 
    return 0;
}

When this header is included (#include) to a file. cpp, each of the variables defined in the header will be copied to this cpp file at the time of inclusion. You can use these constants anywhere in the cpp file.
Because these global constants are linked internally, each .cpp file gets an independent version of the global constant. In most cases, the compiler will optimize them and substitute specific values ​​at the point of use.

Global constants with external binding

The above method has several potential drawbacks. It’s easy to use, but every time we include the constants header file in the code file, each of these variables is copied into the code file. Therefore, if constants.h is included in 20 different code files, each of these variables is duplicated 20 times. Header guard will not prevent this, as it only prevents the header from being included more than once in the same cpp file and not in several different code files. This creates two problems:

  1. Changing one constant will require recompiling each file using the constants, which makes compilation time consuming for large projects.

  2. If the constants are large and cannot be optimized, this will lead to unnecessary memory consumption.

One way to avoid these problems is to provide external binding with these constants, then we can have a single variable (initialized once) that is common to all translation units. Let’s define constants in the .cpp file (to ensure that the definitions only exist in one place) and write the declarations in the header file (which will be included in other cpp files):

// constants.cpp
#include "constants.h"
 
namespace constants
{
    // actual global variables
    extern const double pi { 3.14159 };
    extern const double avogadro { 6.0221413e23 };
    extern const double my_gravity { 9.2 }; // m/s^2 -- gravity is light on this planet
}
// constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
 
namespace constants
{
    // since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
    extern const double pi;
    extern const double avogadro;
    extern const double my_gravity;
}
 
#endif

We can use them:

// main.cpp
#include "constants.h" // include all the forward declarations
 
#include <iostream>
 
int main()
{
    std::cout << "Enter a radius: ";
    int radius{};
    std::cin >> radius;
 
    std::cout << "The circumference is: " << 2.0 * radius * constants::pi << 'n';
 
    return 0;
}

Now constants will only be instantiated once (in constants.cpp), not once every time constants.h is included, and all uses will simply reference the version in constants.cpp. Any changes made to constants.cpp will only require a recompilation of constants.cpp.
However, this method has several disadvantages. First, these constants can now only be considered compile-time constants in the file in which they are actually defined (constants.cpp) and not elsewhere. This means that outside of constants.cpp they cannot be used anywhere where a compile-time constant is required. Second, it is more difficult for the compiler to optimize their use.
Considering the above disadvantages, I would like to define constants in the header file.

Global constants as inline variables

C ++ 17 introduced a new concept called inline variables. In C ++, the term inline evolved to mean “multiple definitions allowed.” Thus, an inline variable is one that can be defined across multiple files without violating the ODR. Inline globals are linked externally by default.
Built-in variables have two main restrictions that must be observed:

  1. All built-in variable definitions must be identical (otherwise it will lead to undefined behavior).

  2. A built-in variable definition must be present in any file that uses the variable.

The linker will combine all inline definitions into one variable definition. This allows us to define variables in the header file and treat them as if there was only one definition somewhere in the .cpp file.
Let’s rewrite our example as follows:

// constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
 
// define your own namespace to hold constants
namespace constants
{
    inline constexpr double pi { 3.14159 }; // note: now inline constexpr
    inline constexpr double avogadro { 6.0221413e23 };
    inline constexpr double my_gravity { 9.2 }; // m/s^2 -- gravity is light on this planet
    // ... other related constants
}
#endif
// main.cpp
#include "constants.h"
 
#include <iostream>
 
int main()
{
    std::cout << "Enter a radius: ";
    int radius{};
    std::cin >> radius;
 
    std::cout << "The circumference is: " << 2.0 * radius * constants::pi << 'n';
 
    return 0;
}

We can include constants.h in any number of cpp files, but these variables will be created only once and shared across all code files.

Initializing static class fields

Consider a class with a static field. In C ++ 14, you need to first declare it in a class:

// my_header.h
#pragma once
#include <string>

struct SomeClass
{
    static std::string myStaticString;
};

And then define it in a separate compilation unit:

//my_src.cpp
#include #my_header.h"
std::string SomeClass::myStaticString{"This is annoying"};

In C ++ 14, defining a variable in a class will result in a compile-time error. However, in C ++ 17 you can do this:

// my_header.h
#pragma once

struct SomeClass
{
    static inline std::string myStaticString{"This is cool"};
};

Defining outside the class is also possible:

// my_header.h
#pragma once

struct SomeClass
{
    static std::string myStaticString;
};

inline std::string SomeClass::myStaticString{"This is cool"};

Remarks

Let me emphasize again: all definitions of a built-in function or variable in the entire program must be identical. Violation of this rule will result in undefined behavior.

A static member variable (but not a namespace variable) declared by constexpr is implicitly a built-in variable.

Example:

// my_header.h
#pragma once

constexpr int generateRandomInt()
{
    // calculate some random value at compile time
}

struct SomeClass
{
    static constexpr int myRandomInt = generateRandomInt();
};

But why don’t we run into problems when defining functions in class header files?

In fact, for functions, everything said above is also relevant, but the compiler makes it easier for us by arranging the word on our own inline in the right places. Or more precisely:

  • A function defined entirely within a class / structure / union definition is implicitly an inline function.

  • A function declared by constexpr is implicitly a built-in function.

  • A deleted function is implicitly a built-in function: its definition (= delete) can appear in multiple translation units.

Links

When preparing the article, in addition to the materials that I referred to in the text, the following were used:

This article was prepared by the OTUS expert – Anatoly Makhaev especially for the students of the course C ++ Developer. Professional

In anticipation of the start of the course, we invite everyone to sign up for a free demo lesson on the topic: “Useful tools in C ++ development”


Similar Posts

Leave a Reply