Ключевое слово inline, наверное, самое непонятое ключевое слово в C++. Я помню, когда я только начинал, большинство онлайн-источников упоминали только inline как способ подсказать компилятору заменить логику функции вместо каждого ее экземпляра; Другими словами, встраивание.

Значит, это просто функция оптимизации? Что ж, в современных современных компиляторах «подсказки» по оптимизации часто опускаются. Не только потому, что компиляторы действительно хорошо умеют оптимизировать код, люди также часто ошибаются, особенно когда дело касается оптимизации.

На данный момент я бы не стал винить кого-либо за то, что он не изучил этот вопрос дальше. Однако ключевое слово inline играет огромную роль в том, как ваш код компилируется и компонуется за кулисами, особенно в отношении Одного правила определения.

Меня очень тревожит, что это недостаточно обсуждается в большинстве ресурсов по C++ для начинающих, но, эй, это то, что я сейчас пытаюсь сделать :)

Что такое правило одного определения?

Проще говоря, ODR утверждает, что во всей программе C++ объект или функция НЕ должны иметь более одного определения. Это правило является причиной того, что что-то подобное может вызвать ошибку компоновщика.

// ========= 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;
}

Если мы попытаемся скомпилировать и связать их, мы, как и ожидалось, получим ошибку компоновщика.

$ 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

Но какое отношение это имеет к inline ?

Inline — отказ от ODR

Помечая функцию (и переменные в C++17) значком inline, мы фактически сообщаем компоновщику, что ее определение может появляться в нескольких единицах перевода, где оно используется ODR. Таким образом, хотя определение указанного символа встречается не только в единицах перевода, программа ведет себя так, как будто существует только одно определение!

Тем не менее, ODR здесь по-прежнему применяется, но в контексте того, что для каждой единицы перевода должно быть только одно определение.

Итак, что дает? Это позволяет программистам определять встроенные символы в файлах заголовков, поскольку они могут существовать в нескольких модулях компиляции.

Поэтому мы можем написать такую ​​программу, определяя наши функции в заголовочных файлах.

// ========= 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;
}

Если мы скомпилируем, свяжем и запустим следующие источники, мы получим ожидаемое поведение!

$ g++ -o app main.cpp beverage.cpp
$ ./app
Here, the best beverage is Dr Pepper!
Here, the best beverage is still Dr Pepper!

Если при определении функции в заголовочном файле у нас нет ключевого слова inline, мы получим ту же ошибку компоновщика, что и раньше, поскольку во всей программе существует более одного определения.

// ========= 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

Неявно встроенный

Примечательно, что методы или функции-члены, определенные в определении класса, являются встроенными функциями.

// ========= 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

В данном случае все три метода: GetExpiryDate(), GetNetVolume() и GetBrand() являются встроенными. Просто разные синтаксисы.

Для полноты, если мы включим этот заголовок в два исходных файла, скомпилируем и свяжем их, мы получим ожидаемое поведение.

// ========= 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

Еще одно важное замечание: функции шаблона также являются встроенными по умолчанию, а полные специализации шаблона – нет. Это означает, что на них распространяется действие ODR, но также подразумевает, что им также может быть inline!

Встроенные переменные C++17

В C++17 появились переменные inline, которые имеют семантику, аналогичную семантике функции inline, но предназначены только для переменных.

Это здорово, поскольку улучшает читаемость кода, например, при инициализации членов класса static. Нам пришлось инициализировать static переменных-членов где-то в исходном файле реализации. Но благодаря inline переменным мы теперь можем объявлять и инициализировать static членов в одном и том же месте!

// ========= 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

«Маленькое» предостережение: избегайте неправильного формата кода.

Однако очень важно отметить, что, поскольку ключевое слово inline фактически отклоняет проверки компоновщика на ODR, это позволяет ему выбирать любое из определений символа во всех единицах перевода, в которых он определен, и игнорировать другие.

Это означает, что ответственность за то, чтобы все определения были одинаковыми, лежит на программисте, иначе программа будет неправильной!

// ========= 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;
}

В случае моего компилятора результат зависит от порядка компиляции, но такое поведение не гарантировано, поэтому не удивляйтесь, если он даст вам пинту 🍺 вместе с ним!

$ g++ -o app main.cpp beverage.cpp
$ ./app
42
42

$ g++ -o app beverage.cpp main.cpp
$ ./app
1
1

Надеюсь, это прояснило влияние inline на ваш код. Мне бы хотелось, чтобы об этом чаще упоминалось в ресурсах C++, но один мудрый человек однажды сказал: «Станьте тем изменением, которое вы хотите видеть в мире» :)

Не стесняйтесь оставлять комментарии, если у вас есть какие-либо сомнения или вы хотите что-то добавить!