6.8 – Глобальные константы и встраиваемые (inline) переменные

Добавлено 11 мая 2021 в 22:12

В некоторых приложениях использование определенных символьных констант может потребоваться во всем коде (а не только в одном месте). Сюда могут входить неизменяемые физические или математические константы (например, Пи или число Авогадро) или значения «настроек» для конкретного приложения (например, коэффициенты трения или силы тяжести). Вместо того, чтобы переопределять эти константы в каждом файле, который в них нуждается (нарушение правила DRY, «Don’t Repeat Yourself», «не повторяйся»), лучше объявить их один раз в центральном месте и использовать везде, где они необходимы. Таким образом, если вам когда-либо понадобится их изменить, вам нужно будет изменить их только в одном месте, и эти изменения распространятся на весь код.

В этом уроке обсуждаются наиболее распространенные способы сделать это.

Глобальные константы как внутренние переменные

В C++ есть несколько способов облегчить эту задачу. До C++17, вероятно, наиболее простым и распространенным являлся следующий:

  1. создать заголовочный файл для хранения этих констант;
  2. внутри этого заголовочного файла определить пространство имен (обсуждается в уроке «6.2 – Пользовательские пространства имен»);
  3. добавить все свои константы в это пространство имен (убедитесь, что они являются constexpr);
  4. включить с помощью #include этот заголовочный файл везде, где он вам нужен.

Например:

constants.h:

#ifndef CONSTANTS_H
#define CONSTANTS_H
 
// определяем собственное пространство имен для хранения констант
namespace constants
{
    // константы по умолчанию имеют внутреннее связывание
    constexpr double pi { 3.14159 };
    constexpr double avogadro { 6.0221413e23 };
    constexpr double my_gravity { 9.2 }; // m/s^2 - гравитация на этой планете слабая
    // ... другие связанные констант
}
#endif

Затем, чтобы получить доступ к вашим константам в файлах .cpp, используйте оператор разрешения области видимости (::) с именем пространства имен слева и именем вашей переменной справа:

main.cpp:

#include "constants.h" // включаем в этот файл копию каждой константы
 
#include <iostream>
 
int main()
{
    std::cout << "Enter a radius: ";
    int radius{};
    std::cin >> radius;
 
    std::cout << "The circumference is: " << 2.0 * radius * constants::pi << '\n';
 
    return 0;
}

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

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

В качестве отступления...


Термин «оптимизация» относится к любому процессу, в котором компилятор оптимизирует производительность вашей программы, удаляя элементы таким образом, чтобы это не влияло на результаты работы программы. Например, допустим, у вас есть некоторая константная переменная x, которая инициализирована значением 4. Везде, где ваш код ссылается на эту переменную x, компилятор может просто заменить x на 4 (поскольку x является константой, мы знаем, что она никогда не изменится на другое значение) и вообще избежать создания и инициализации переменной.

Глобальные константы как внешние переменные

Показанный выше метод имеет несколько потенциальных недостатков.

Хотя он простой (и нормальный для небольших программ), каждый раз, когда constants.h включается в другой файл исходного кода, каждая из этих переменных копируется во включающий его файл. Следовательно, если constants.h включается в 20 различных файлов кода, каждая из этих переменных дублируется 20 раз. Защита заголовков не предотвратит этого, так как она предотвращает включение заголовка более одного раза только в один включающий файл, а не одноразовое включение в несколько разных исходных файлов. Это создает две проблемы:

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

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

Примечание автора


В этом методе мы используем const вместо constexpr, потому что переменные constexpr не могут быть предварительно объявлены, даже если они имеют внешнее связывание.

constants.cpp:

#include "constants.h"
 
namespace constants
{
    // реальные глобальные переменные
    extern const double pi { 3.14159 };
    extern const double avogadro { 6.0221413e23 };
    extern const double my_gravity { 9.2 }; // m/s^2 - гравитация на этой планете слабая
}

constants.h:

#ifndef CONSTANTS_H
#define CONSTANTS_H
 
namespace constants
{
    // поскольку реальные переменные находятся внутри пространства имен,
    // предварительные объявления также должны находиться внутри пространства имен
    extern const double pi;
    extern const double avogadro;
    extern const double my_gravity;
}
 
#endif

Использование в файле исходного кода остается прежним:

main.cpp:

#include "constants.h" // включаем все предварительные объявления
 
#include <iostream>
 
int main()
{
    std::cout << "Enter a radius: ";
    int radius{};
    std::cin >> radius;
 
    std::cout << "The circumference is: " << 2.0 * radius * constants::pi << '\n';
 
    return 0;
}

Поскольку глобальные символьные константы должны иметь пространство имен (чтобы избежать конфликтов имен с другими идентификаторами в глобальном пространстве имен), использование префикса "g_" в именах не требуется.

Теперь символьные константы будут создаваться только один раз (в constants.cpp), а не в каждом файле кода, где включается constants.h, и все использования этих констант будут связаны с версией, экземпляр которой создан в constants.cpp. Любые изменения, внесенные в constants.cpp, потребуют перекомпиляции только constants.cpp.

Однако у этого метода есть несколько недостатков. Во-первых, эти константы теперь считаются константами времени компиляции только в том файле, в котором они фактически определены (constants.cpp). В других файлах компилятор увидит только предварительное объявление, которое не определяет постоянного значения (и должно быть разрешено компоновщиком). Это означает, что в других файлах они рассматриваются как значения констант времени выполнения, а не как константы времени компиляции. Таким образом, за пределами constants.cpp эти переменные будет нельзя использовать везде, где требуется константа времени компиляции. Во-вторых, поскольку константы времени компиляции обычно можно оптимизировать лучше, чем константы времени выполнения, компилятор не сможет оптимизировать их так же хрошо.

Ключевые выводы


Чтобы переменные можно было использовать в контекстах времени компиляции (таких как размеры массивов), компилятор должен видеть определение переменной (а не только предварительное объявление).

Поскольку компилятор компилирует каждый исходный файл отдельно, он может видеть только определения переменных, которые появляются в компилируемом исходном файле (который включает любые включенные заголовки). Например, определения переменных в constants.cpp не видны, когда компилятор компилирует main.cpp. По этой причине переменные constexpr не могут быть разделены на заголовочный и исходный файлы, они должны быть определены в заголовочном файле.

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

Глобальные константы как встраиваемые (inline) переменные

C++17 представил новую концепцию, называемую встраиваемыми (inline) переменными. В C++ термин «встраиваемый» (inline) стал обозначать «разрешено несколько определений». Таким образом, встраиваемая переменная может быть определена в нескольких файлах без нарушения правила одного определения. Встраиваемые глобальные переменные по умолчанию имеют внешнее связывание.

У встраиваемых переменных есть два основных ограничения, которые необходимо соблюдать:

  1. все определения встраиваемой переменной должны быть идентичными (иначе в результате будет получено неопределенное поведение);
  2. определение встраиваемой переменной (не предварительное объявление) должно присутствовать в любом файле, который использует эту переменную.

Компоновщик объединит все встраиваемые определения переменной в одно определение переменной (таким образом, соблюдая правило одного определения). Это позволяет нам определять переменные в заголовочном файле и, чтобы они обрабатывались так, как если бы каком-то в файле .cpp было только одно определение. Эти переменные также сохраняют своё constexpr-ство во всех файлах, в которые они включены.

Благодаря этому мы можем вернуться к определению наших глобальных переменных в заголовочном файле без недостатков дублирования переменных:

constants.h:

#ifndef CONSTANTS_H
#define CONSTANTS_H
 
// определяем собственное пространство имен для хранения констант
namespace constants
{
    inline constexpr double pi { 3.14159 }; // обратите внимание: теперь inline constexpr
    inline constexpr double avogadro { 6.0221413e23 };
    inline constexpr double my_gravity { 9.2 }; // m/s^2 -- гравитация на этой планете слабая
    // ... другие связанные константы
}
#endif

main.cpp:

#include "constants.h"
 
#include <iostream>
 
int main()
{
    std::cout << "Enter a radius: ";
    int radius{};
    std::cin >> radius;
 
    std::cout << "The circumference is: " << 2.0 * radius * constants::pi << '\n';
 
    return 0;
}

Мы можем включить constants.h в любое количество файлов исходного кода, но эти переменные будут созданы только один раз и будут использоваться во всех исходных файлах.

Лучшая практика


Если вам нужны глобальные константы, и ваш компилятор поддерживает C++17, предпочтительным способом будет определение глобальных переменных в заголовочном файле как inline constexpr.

Теги

C++ / CppconstexprinlineLearnCppГлобальная переменнаяДля начинающихКонстантаОбласть видимостиОбучениеПрограммированиеСимвольная константа

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.