4.14 – const, constexpr и символьные константы

Добавлено 2 мая 2021 в 15:36

Константные (постоянные) переменные

До сих пор все переменные, которые мы видели, были непостоянными, то есть их значения можно изменить в любое время. Например:

int x { 4 }; // инициализируем x значением 4
x = 5;       // меняем значение x на 5 

Однако иногда бывает полезно определять переменные со значениями, которые нельзя изменить. Например, рассмотрим ускорение свободного падения у поверхности Земли: 9,8 м/с2. Маловероятно, что в ближайшее время оно изменится (а если это произойдет, у вас, вероятно, возникнут более серьезные проблемы, чем изучение C++). Определение этого значения как константы помогает гарантировать, что оно не будет случайно изменено.

Чтобы сделать переменную константой, просто поместите ключевое слово const до или после типа переменной, например:

const double gravity { 9.8 };  // предпочтительное использование const перед типом
int const sidesInSquare { 4 }; // хорошо, но не рекомендуется

Хотя C++ принимает const до или после типа, мы рекомендуем использовать константу перед типом, потому что это лучше соответствует соглашению обычного английского языка, согласно которому модификаторы ставятся перед изменяемым объектом (например, «green ball» (зеленый шар), а не «ball green» (шар зеленый)).

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

Объявление переменной как const предотвращает непреднамеренное изменение ее значения:

const double gravity { 9.8 };
gravity = 9.9; // не допускается, это вызовет ошибку компиляции

Определение константной переменной без ее инициализации также вызовет ошибку компиляции:

const double gravity; // ошибка компиляции, должна быть инициализирована при определении

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

std::cout << "Enter your age: ";
int age{};
std::cin >> age;
 
const int usersAge { age }; // usersAge изменить нельзя

const часто используется с параметрами функции:

void printInteger(const int myValue)
{
    std::cout << myValue;
}

Задание параметра функции константой делает две вещи. Во-первых, это сообщает человеку, вызывающему функцию, что функция не изменит значение myValue. Во-вторых, это гарантирует, что функция не изменит значение myValue.

Когда аргументы передаются по значению, нас обычно не волнует, изменяет ли функция значение параметра (поскольку это всего лишь копия, которая в любом случае будет уничтожена в конце функции). По этой причине мы обычно не делаем константными параметры, передаваемые по значению. Но позже мы поговорим о других типах параметров функций (где изменение значения параметра приведет к изменению значения переданного аргумента). Для этих типов параметров важно разумное использование const.

Константы времени выполнения и константы времени компиляции

На самом деле C++ имеет два разных типа констант.

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

Константы времени компиляции – это те, чьи значения инициализации могут быть вычислены во время компиляции (когда ваша программа компилируется). Переменная gravity выше является примером постоянной времени компиляции. Константы времени компиляции позволяют компилятору выполнять оптимизацию, недоступную для констант времени выполнения. Например, всякий раз, когда используется gravity, компилятор может просто заменить идентификатор gravity литералом 9.8 типа double.

Когда вы объявляете константную переменную, компилятор неявно отслеживает, является ли она константой времени выполнения или константой времени компиляции.

В большинстве случаев это не имеет значения, но есть несколько странных случаев, когда C++ требует константу времени компиляции вместо константы времени выполнения, например, при создании экземпляра типа – о чем мы поговорим позже.

constexpr

Чтобы обеспечить большую конкретность, в C++11 введено ключевое слово constexpr, которое гарантирует, что константа должна быть константой времени компиляции:

constexpr double gravity { 9.8 }; // хорошо, значение 9,8 может быть определено во время компиляции
constexpr int sum { 4 + 5 };      // хорошо, значение 4 + 5 может быть определено во время компиляции
 
std::cout << "Enter your age: ";
int age{};
std::cin >> age;
constexpr int myAge { age }; // плохо, age не может быть определен во время компиляции

Переменные constexpr являются константными. Это станет важным, когда мы поговорим о других влияниях const в следующих уроках.

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


Любую переменную, которую нельзя изменять после инициализации, и инициализатор которой известен во время компиляции, следует объявлять как constexpr.

Любую переменную, которую нельзя изменять после инициализации, и инициализатор которой неизвестен во время компиляции, следует объявлять как const.

Именование ваших константных переменных

Некоторые программисты для константных переменных предпочитают использовать имена полностью из заглавных букв. Другие используют обычные имена переменных с префиксом 'k'. Однако мы будем использовать обычные соглашения об именах переменных, которые встречаются чаще. Константные переменные действуют точно так же, как обычные переменные во всех случаях, за исключением того, что им не может быть присвоено другое значение, поэтому нет особой причины, по которой они должны обозначаться как-то по-особенному.

Символьные константы

В предыдущем уроке «4.13 – Литералы» мы обсуждали «магические числа», которые представляют собой литералы, используемые в программе для представления постоянного значения. Что делать, если магические числа – это плохо? Ответ: используйте символические константы! Символьная константа – это имя, данное константному литеральному значению. В C++ есть два способа объявить символьную константу. Один из них хороший, а один нет. Мы покажем вам оба.

Плохое решение: использование объекто-подобных макросов с параметром подстановки в качестве символьных констант

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

В уроке «2.9 – Знакомство с препроцессором» вы узнали, что у объекто-подобных макросов есть две формы: одна не принимает параметр подстановки (обычно используется для условной компиляции), а другая имеет параметр подстановки. Здесь мы поговорим о случае с параметром подстановки. Он имеет следующую форму:

#define идентификатор подставляемый_текст

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

Рассмотрим следующий фрагмент:

#define MAX_STUDENTS_PER_CLASS 30
int max_students { numClassrooms * MAX_STUDENTS_PER_CLASS };

Когда вы компилируете свой код, препроцессор заменяет все экземпляры MAX_STUDENTS_PER_CLASS литеральным значением 30, которое затем компилируется в ваш исполняемый файл.

Вы, вероятно, согласитесь, что это по нескольким причинам гораздо более интуитивно понятно, чем использование магического числа. MAX_STUDENTS_PER_CLASS даже без комментария предоставляет контекст для того, что программа пытается сделать. Во-вторых, если количество студентов в классе изменяется, нам нужно изменить значение MAX_STUDENTS_PER_CLASS только в одном месте, и все экземпляры MAX_STUDENTS_PER_CLASS при следующей компиляции будут заменены новым литеральным значением.

Рассмотрим наш второй пример с использованием символьных констант #define:

#define MAX_STUDENTS_PER_CLASS 30
#define MAX_NAME_LENGTH 30
 
int max_students { numClassrooms * MAX_STUDENTS_PER_CLASS };
setMax(MAX_NAME_LENGTH);

В этом случае очевидно, что MAX_STUDENTS_PER_CLASS и MAX_NAME_LENGTH должны быть независимыми, даже если они имеют одно и то же значение (30). Таким образом, если нам нужно обновить размер класса, мы не сможем случайно изменить длину имени.

Так почему бы не использовать #define для создания символьных констант? Есть (по крайней мере) три основных проблемы.

Во-первых, поскольку макросы вычисляются препроцессором, который заменяет символьное имя определенным значением, символьные константы, определенные через #define, не отображаются в отладчике (который показывает ваш фактический код). Таким образом, хотя компилятор будет компилировать int max_students {numClassrooms * 30};, в редакторе кода вы увидите int max_students {numClassrooms * MAX_STUDENTS_PER_CLASS};, и MAX_STUDENTS_PER_CLASS не будет отслеживаться в отладчике. Вам нужно будет найти определение MAX_STUDENTS_PER_CLASS, чтобы узнать его фактическое значение. Это может затруднить отладку ваших программ.

Во-вторых, макросы могут конфликтовать с обычным кодом. Например:

#include "someheader.h"
#include <iostream>
 
int main()
{
    int beta { 5 };
    std::cout << beta;
 
    return 0;
}

Если в someheader.h появляется определение с помощью #define макроса с именем beta, эта простая программа сломается, так как препроцессор заменит имя целочисленной переменной beta на какое-то значение макроса.

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

Предупреждение


Избегайте использования #define для создания макросов символьных констант.

Лучшее решение: используйте переменные constexpr

Лучший способ создать символьные константы – использовать переменные constexpr:

constexpr int maxStudentsPerClass { 30 };
constexpr int maxNameLength { 30 };

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

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


Используйте переменные constexpr, чтобы указать имя и контекст для ваших магических чисел.

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

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

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

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

Например:

constants.h (C++11/14):

#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

В C++17 лучше использовать inline constexpr:

constants.h (C++17 или новее):

#ifndef CONSTANTS_H
#define CONSTANTS_H
 
// определяем собственное пространство имен для хранения констант
namespace constants
{
    inline constexpr double pi { 3.14159 }; // inline constexpr - только для C++17 или новее
    inline constexpr double avogadro { 6.0221413e23 };
    inline 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;
 
    double circumference { 2.0 * radius * constants::pi };
    std::cout << "The circumference is: " << circumference << '\n';
 
    return 0;
}

Если у вас есть и физические константы, и значения настроек для каждого приложения, вы можете выбрать использование двух наборов файлов: один для физических значений, которые никогда не изменятся, а другой для специфичных значений настроек отдельно для каждой программы. Таким образом, вы можете повторно использовать физические значения в любой программе.

Теги

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

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

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