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


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.