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


124

На собрании стандартов ISO C ++ в Оулу в 2016 году комитет по стандартам проголосовал за предложение под названием « Встроенные переменные» в C ++ 17.

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


@jotik Я предполагаю, что эквивалентной операцией будет замена любого вхождения переменной ее значением. Обычно это действительно только в том случае, если переменная есть const.
melpomene

5
Это не единственное, что inlineключевое слово выполняет для функций. inlineКлючевое слово, применительно к функциям, есть еще один важный эффект, который транслирует непосредственно к переменным. inlineФункция , которая предположительно объявлена в заголовочном файле, не приведет «дубликат символ» ошибка во время компоновки, даже если заголовок получает #included нескольких единиц перевода. inlineКлючевое слово, применительно к переменным, будет иметь точно такой же результат. Конец.
Сэм Варшавчик

4
^ В смысле «заменить любой вызов этой функции на копию ее кода на месте» inline- это лишь слабый, необязательный запрос к оптимизатору. Компиляторы могут не встраивать запрошенные функции и / или встраивать те, которые вы не аннотировали. Скорее, фактическая цель inlineключевого слова состоит в том, чтобы обойти множественные ошибки определения.
underscore_d

Ответы:


121

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

»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также есть намек на оптимизацию, что компилятор должен предпочесть заменять вызовы этой функции прямой заменой машинного кода функции. Этот намек можно игнорировать.


2
Кроме того, ограничение const применяется только к переменным области пространства имен. Те, которые относятся к классу (например, Kath::hi), не обязательно должны быть константными.
TC

4
В более новых отчетах указано, что constограничение полностью снято.
TC

2
@Nick: Поскольку Ричард Смит (нынешний «редактор проекта» комитета по C ++) является одним из двух авторов, и поскольку он является «владельцем кода интерфейса Clang C ++», предположил Clang. И конструкция скомпилирована с помощью clang 3.9.0 на Godbolt . Он предупреждает, что встроенные переменные являются расширением C ++ 1z. Я не нашел способа поделиться выбором и параметрами источника и компилятора, поэтому ссылка только на сайт в целом, извините.
Приветствия и hth. - Alf

1
зачем нужно ключевое слово inline внутри объявления класса / структуры? Почему бы просто не разрешить static std::string const hi = "Zzzzz...";?
саша.сочка

2
@EmilianCioca: Нет, вы столкнетесь с фиаско с порядком статической инициализации . Синглтон - это, по сути, способ избежать этого.
Приветствия и hth. - Альф

16

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

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

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

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

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

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

inline T global;

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

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

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


1
Не могли бы вы подробнее рассказать о проблемах с производительностью синглтона Мейера?
Юрай Оршулич

@ JurajOršulić В большинстве случаев накладные расходы на шаблон синглтонов Мейера невелики. Компиляторы сильно оптимизируют быстрый путь (то есть доступ к уже инициализированной переменной). Тем не менее, это накладные расходы, поскольку вам необходимо предоставить потокобезопасный код для обработки отложенной инициализации. Для подробностей, языковая функция иногда называется «магической статикой» и была представлена ​​в C ++ 11. В документе N2660 содержится информация о том, как компиляторы могут эффективно реализовать его, чтобы свести накладные расходы к минимуму: open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2660.htm#Appendix
Филипп Классен

1
Итак, вы говорили о накладных расходах, связанных с поточно-ориентированной инициализацией магической статики, в отличие от использования вашей собственной небезопасной версии с шаблонами (как бы вы это сделали, если бы не использовали эти недавно представленные встроенные Глобал?). Спасибо за уточнение.
Юрай Оршулич

11

Минимальный запускаемый пример

Эта замечательная функция 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 вверх по течению .

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

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

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

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 вверх по течению .

Альтернативы только для заголовков до 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 ++ 17, я загрузил пример по адресу: Что означает «статический» в C?

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

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

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

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

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

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

Связанный:

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


2
inlineпочти не имеет отношения к встраиванию ни для функций, ни для переменных, несмотря на само слово. inlineне говорит компилятору что-либо встроить. Он говорит компоновщику убедиться, что существует только одно определение, что традиционно было работой программиста. Итак, "Есть ли способ полностью встроить его?" это как минимум вопрос совершенно не связанный.
not-a user
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.