Как работают встроенные переменные?
На совещании по стандартам Oulu ISO C++ 2016 года комитет по стандартам проголосовал за предложение под названием " Встроенные переменные" в C++17.
С точки зрения непрофессионала, что такое встроенные переменные, как они работают и для чего они полезны? Как встроенные переменные должны быть объявлены, определены и использованы?
2 ответа
Первое предложение предложения:
”
inline
Спецификатор может применяться как к переменным, так и к функциям.
Гарантированный эффект inline
применительно к функции - позволяет идентифицировать функцию идентично, с внешней связью, в нескольких единицах перевода. На практике это означает определение функции в заголовке, которая может быть включена в несколько единиц перевода. Предложение распространяет эту возможность на переменные.
Таким образом, в практическом плане (в настоящее время принято) предложение позволяет использовать inline
ключевое слово для определения внешней связи const
переменная области имен пространства или любой static
элемент данных класса в файле заголовка, так что несколько определений, которые появляются, когда этот заголовок включен в несколько единиц перевода, в порядке с компоновщиком - он просто выбирает один из них.
Вплоть до и включая C++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`.
Начиная с C++17 и далее, я считаю, что можно написать только
struct Kath
{
static std::string const hi;
};
inline std::string const Kath::hi = "Zzzzz..."; // Simpler!
... в заголовочном файле.
Предложение включает в себя формулировку
" Встроенный статический член данных может быть определен в определении класса и может указывать инициализатор фигурной скобки или равнозначный. Если член объявлен с
constexpr
спецификатор, он может быть повторно объявлен в области пространства имен без инициализатора (это использование устарело; см. DX). Объявления других членов статических данных не должны указывать инициализатор в скобках или равно
… Что позволяет еще больше упростить вышеизложенное до
struct Kath
{
static inline std::string const hi = "Zzzzz..."; // Simplest!
};
… Как отмечено TC в комментарии к этому ответу.
Так же constexpr
Спецификатор подразумевает inline
для статических данных членов, а также функций.
Заметки:
¹ Для функции inline
также имеет хинтинговый эффект в отношении оптимизации: компилятор должен предпочесть заменить вызовы этой функции прямой заменой машинного кода функции. Этот намек можно игнорировать.
Встроенные переменные очень похожи на встроенные функции. Он сообщает компоновщику, что должен существовать только один экземпляр переменной, даже если переменная видна в нескольких единицах компиляции. Компоновщик должен убедиться, что копии больше не создаются.
Встроенные переменные могут использоваться для определения глобальных переменных в библиотеках только заголовков. До C++17 им приходилось использовать обходные пути (встроенные функции или взлом шаблонов).
Например, одним из обходных путей является использование синглтона Мейера со встроенной функцией:
inline T& instance()
{
static T global;
return global;
}
У этого подхода есть некоторые недостатки, в основном с точки зрения производительности. Эти издержки можно избежать с помощью шаблонных решений, но их легко ошибиться.
С помощью встроенных переменных вы можете напрямую объявить это (не получая ошибку компоновщика с несколькими определениями):
inline T global;
Помимо библиотек только с заголовками, в других случаях могут помочь встроенные переменные. Нир Фридман освещает эту тему в своем выступлении на CppCon: Что разработчики C++ должны знать о глобальных переменных (и компоновщике). Часть о встроенных переменных и обходных путях начинается в 18m9s.
Короче говоря, если вам нужно объявить глобальные переменные, которые совместно используются единицами компиляции, объявить их как встроенные переменные в заголовочном файле просто и избежать проблем с обходными путями до C++17.
(Есть еще варианты использования для синглтона Мейера, например, если вы явно хотите иметь ленивую инициализацию.)
Минимальный исполняемый пример
Эта удивительная особенность C++17 позволяет нам:
- удобно использовать только один адрес памяти для каждой константы
- хранить его как
constexpr
: Как объявить constexpr extern? - сделать это в одной строке из одного заголовка
main.cpp
#include <cassert>
#include "notmain.hpp"
int main() {
// Both files see the same memory address.
assert(¬main_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 ¬main_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
Смотрите также: Как работают встроенные переменные?
Стандарт C++ для встроенных переменных
Стандарт C++ гарантирует, что адреса будут одинаковыми. Проект стандарта C++17 N4659 10.1.6 " Встроенный спецификатор":
6 Встроенная функция или переменная с внешней связью должны иметь одинаковый адрес во всех единицах перевода.
cppreference https://en.cppreference.com/w/cpp/language/inline объясняет, что если static
не дано, то имеет внешнюю связь.
Реализация встроенных переменных
Мы можем наблюдать, как это реализовано с:
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.
Pre-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(¬main_i == notmain_func());
assert(notmain_i == 42);
}
notmain.cpp
#include "notmain.hpp"
const int notmain_i = 42;
const int* notmain_func() {
return ¬main_i;
}
notmain.hpp
#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
extern const int notmain_i;
const int* notmain_func();
#endif
Любой способ полностью встроить это?
TODO: есть ли способ полностью встроить переменную без использования памяти вообще?
Очень похоже на то, что делает препроцессор.
Это потребует как-то:
- Запрещение или обнаружение, если адрес переменной взят
- добавить эту информацию в объектные файлы ELF и позволить LTO оптимизировать ее
Связанные с:
Протестировано в Ubuntu 18.10, GCC 8.2.0.