Как работают встроенные переменные?

На собрании стандартов Oulu ISO С++ 2016 года предложение по стандарту Inline Variables было проголосовано в С++ 17 комитетом по стандартам.

В условиях неспециалиста, что такое встроенные переменные, как они работают и для чего они полезны? Как объявлять, определять и использовать встроенные переменные?

Ответы

Ответ 1

Первое предложение предложения:

" Спецификатор inline может применяться как к переменным, так и к функциям.

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

Итак, с практической точки зрения (теперь принятое) предложение позволяет использовать ключевое слово inline для определения переменной области видимости внешнего связывания const или любого члена данных класса static в файле заголовка, поэтому что множественные определения, которые возникают, когда этот заголовок включен в несколько единиц перевода, в порядке с компоновщиком – он просто выбирает один из них.

До и включая С++ 14 для этого были созданы внутренние механизмы, чтобы поддерживать переменные static в шаблонах классов, но не было удобного способа использования этого оборудования. Приходилось прибегать к трюкам вроде

template< class Dummy >
struct Kath_
{
    static std::string const hi;
};

template< class Dummy >
std::string const Kath_<Dummy>::hi = "Zzzzz...";

using Kath = Kath_<void>;    // Allows you to write `Kath::hi`.

Из С++ 17 и далее я считаю, что можно просто написать

struct Kath
{
    static std::string const hi;
};

inline std::string const Kath::hi = "Zzzzz...";    // Simpler!

& hellip; в файле заголовка.

Предложение включает формулировку

" Встроенный элемент статических данных может быть определен в определении класса и может генерировать скользящий или равный инициализатор. Если член объявлен с помощью спецификатора constexpr, он может быть переопределен в области пространства имен без инициализатора (это использование устарело, см. DX). Объявления других статических членов данных не должны указывать итератор с фигурной скобкой или равным

& hellip; что позволяет упростить приведенное выше просто

struct Kath
{
    static inline std::string const hi = "Zzzzz...";    // Simplest!
};

& hellip; как отметил T.C в комментарий к этому ответу.

Кроме того, спецификатор ​constexpr подразумевает inline для статических членов данных, а также для функций.


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

Ответ 2

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

Встроенные переменные могут использоваться для определения глобальных переменных в библиотеках только заголовков. До C++ 17 им приходилось использовать обходные пути (встроенные функции или взлом шаблонов).

Например, одним из обходных путей является использование синглтона Мейера со встроенной функцией:

inline T& instance()
{
  static T global;
  return global;
}

У этого подхода есть некоторые недостатки, в основном с точки зрения производительности. Эти издержки можно избежать с помощью шаблонных решений, но их легко ошибиться.

С помощью встроенных переменных вы можете напрямую объявить это (не получая ошибку компоновщика с несколькими определениями):

inline T global;

Помимо библиотек только с заголовками, в других случаях могут помочь встроенные переменные. Нир Фридман освещает эту тему в своем выступлении на CppCon: Что разработчики C++ должны знать о глобальных переменных (и компоновщике). Часть о встроенных переменных и обходных путях начинается в 18m9s.

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

(Есть еще варианты использования для синглтона Мейера, например, если вы явно хотите иметь ленивую инициализацию.)

Ответ 3

Пример минимального запуска

Эта удивительная функция C++ 17 позволяет нам:

  • удобно использовать только один адрес памяти для каждой константы
  • сохранить его как constexpr: Как объявить constexpr extern?
  • сделать это в одной строке из одного заголовка

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

Скомпилируйте и запустите:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub upstream.

Смотрите также: Как работают встроенные переменные?

C++ стандарт для встроенных переменных

Стандарт C++ гарантирует, что адреса будут одинаковыми. C++ 17 N4659 стандартный черновик 10.1.6 "Встроенный спецификатор":

6 Встроенная функция или переменная с внешней связью должны иметь одинаковый адрес во всех единицах перевода.

cppreference https://en.cppreference.com/w/cpp/language/inline объясняет, что если static не указан, то он имеет внешнюю связь.

Реализация встроенной переменной GCC

Мы можем наблюдать, как это реализовано с помощью:

nm main.o notmain.o

который содержит:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

и man nm говорит о u:

"U" Символ является уникальным глобальным символом. Это расширение GNU для стандартного набора привязок символов ELF. Для такого символа динамический компоновщик будет следить за тем, чтобы во всем процессе                есть только один символ с этим именем и используемым типом.

поэтому мы видим, что для этого есть выделенное расширение ELF.

До C++ 17: extern const

До C++ 17 и в C мы можем добиться очень похожего эффекта с extern const, что приведет к использованию одной ячейки памяти.

Недостатки inline:

  • с помощью этого метода невозможно создать переменную constexpr, только inline позволяет это: Как объявить constexpr extern?
  • это менее элегантно, так как вы должны объявить и определить переменную отдельно в заголовочном файле и в файле cpp

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.cpp

#include "notmain.hpp"

const int notmain_i = 42;

const int* notmain_func() {
    return &notmain_i;
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

extern const int notmain_i;

const int* notmain_func();

#endif

GitHub upstream.

Предварительно C++ 17 заголовков только альтернативы

Они не так хороши, как решение extern, но они работают и занимают только одну область памяти:

Функция constexpr, поскольку constexpr подразумевает inline и inline , позволяет (заставляет) определение появляться в каждой единице перевода:

constexpr int shared_inline_constexpr() { return 42; }

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

Вы также можете использовать статическую целочисленную переменную const или constexpr, как в:

#include <iostream>

struct MyClass {
    static constexpr int i = 42;
};

int main() {
    std::cout << MyClass::i << std::endl;
    // undefined reference to 'MyClass::i'
    //std::cout << &MyClass::i << std::endl;
}

но вы не можете делать такие вещи, как получение его адреса, иначе он будет использоваться odr, см. также: https://en.cppreference.com/w/cpp/language/static "Постоянные статические члены" и Определение членов статических данных constexpr

C

В C ситуация такая же, как в C++ pre C++ 17, я загрузил пример по адресу: Что такое "статический"? значит в С?

Единственное отличие состоит в том, что в C++ const подразумевает static для глобалов, но не в семантике C: C++ "static const" против "const"

Есть ли способ полностью встроить его?

TODO: есть ли способ полностью встроить переменную без использования памяти вообще?

Очень похоже на то, что делает препроцессор.

Это потребует как-то:

  • Запрещение или обнаружение, если адрес переменной взят
  • добавить эту информацию в объектные файлы ELF и позволить LTO оптимизировать ее

Связанный:

Протестировано в Ubuntu 18.10, GCC 8.2.0.