Як працюють вбудовані змінні?


124

На засіданні стандартів стандартів ISO C ++ в Оулу 2016 року комітет зі стандартів проголосував за пропозицію " Вбудовані змінні" за С ++ 17.

Зрозуміло, мирянин, що таке вбудовані змінні, як вони працюють і для чого вони корисні? Як слід оголошувати, визначати та використовувати вбудові змінні?


@jotik Я думаю, що еквівалентна операція замінить будь-яке виникнення змінної своїм значенням. Зазвичай це справедливо лише в разі змінної const.
мельпомена

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

4
^ У сенсі "замінити будь-який виклик цієї функції внутрішньою копією свого коду" inlineє лише слабким, не обов'язковим запитом до оптимізатора. Компілятори можуть не вбудовувати запитувані функції та / або вбудовувати ті, які ви не коментували. Швидше, власне мета inlineключового слова - обійти кілька помилок визначення.
підкреслюй_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!
};

... як зазначив ТЦ у коментарі до цієї відповіді.

Також  ​constexprспецифікатор має inline на увазі  для статичних членів даних, а також функції.


Примітки:
¹ Для функції inlineтакож впливає на оптимізацію, що компілятор повинен віддавати перевагу заміні викликів цієї функції прямою підміною машинного коду функції. Цей натяк можна ігнорувати.


2
Також обмеження const стосується лише змінних областей простору імен. Ті, Kath::hiщо стосуються класу (як ), не повинні бути const.
ТК

4
Новіші звіти свідчать, що constобмеження повністю скасовано.
ТК

2
@Nick: Оскільки Річард Сміт (нинішній редактор проекту C ++) "є одним із двох авторів, і оскільки він є" власником фронту "Clang C ++", здогадався Кланг. І конструкція, зібрана з клаксом 3.9.0 на Godbolt . Він попереджає, що вбудовані змінні є розширенням C ++ 1z. Я не знайшов способу поділитися вибором джерела та компілятора та варіантами, тому посилання саме на сайт загалом, вибачте.
Ура та хт. - Альф

1
навіщо потрібне вбудоване ключове слово всередині оголошення класу / структури? Чому не дозволяють просто static std::string const hi = "Zzzzz...";?
sasha.sochka

2
@EmilianCioca: Ні, ви зіткнулися з фіаско статичного порядку ініціалізації . Однотонний по суті є пристроєм, щоб цього уникнути.
Ура та хт. - Альф

15

Вбудовані змінні дуже схожі на вбудовані функції. Це сигналізує лінкеру про те, що має існувати лише один екземпляр змінної, навіть якщо змінна бачиться у кількох одиницях компіляції. Лінкер повинен забезпечити, щоб більше копій не було створено.

Вбудовані змінні можуть використовуватися для визначення глобалів у бібліотеках, лише в заголовках. Перед C ++ 17 їм довелося використовувати обхідні шляхи (вбудовані функції або хакерські шаблони).

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

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

У цього підходу є деякі недоліки, здебільшого щодо ефективності. Цю накладну допомогу можна уникнути шаблоновими рішеннями, але їх неправильно зрозуміти.

За допомогою вбудованих змінних ви можете безпосередньо оголосити це (не отримуючи помилку лінкера множинного визначення):

inline T global;

Крім бібліотек лише заголовків, є й інші випадки, коли вбудовані змінні можуть допомогти. Нір Фрідман висвітлює цю тему у своїй розмові на CppCon: Що розробники C ++ повинні знати про глобальні підприємства (та про лінкер) . Частина про вбудовані змінні та обхідні шляхи починається з 18м9с .

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

(Досі, наприклад, є сингл Мейєра, якщо ви явно хочете мати ліниву ініціалізацію.)


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 ++ гарантує, що адреси будуть однаковими. 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.

Pre-C ++ 17: extern const

Перед C ++ 17 і C, ми можемо досягти дуже схожого ефекту з an 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 вище за течією .

Замінники заголовка Pre-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 ++ семантиці `статичного const` проти` const`

Будь-який спосіб його повністю накреслити?

TODO: Чи є спосіб повністю вбудувати змінну, не використовуючи взагалі ніякої пам'яті?

Наче схоже на те, що робить препроцесор.

Для цього потрібно якось:

  • заборона або виявлення, чи взята адреса змінної
  • додайте цю інформацію до об’єктних файлів ELF, і дозвольте LTO оптимізувати її

Пов'язані:

Випробувано в Ubuntu 18.10, GCC 8.2.0.


2
inlineмайже не має нічого спільного з вбудовою, ні для функцій, ні для змінних, незважаючи на саме слово. inlineне каже компілятору нічого вбудовувати. Він повідомляє лінкеру переконатися, що існує лише одне визначення, яке традиційно є завданням програміста. Тож "Будь-який спосіб його повністю вбудувати?" принаймні, абсолютно не пов'язане питання.
не-користувач
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.