1
В случае A
существует разница между глобальной областью или областью пространства имен (внутренняя связь) и областью действия класса (внешняя связь). Так
// header.hpp
constexpr T A = <some value>; // internal linkage
namespace Nm { constexpr T A = <some value>; } // internal linkage
class Cl { public: static constexpr T A = <some value>; }; // not enough!
Рассмотрим следующее использование:
// user.cpp
std::cout << A << Nm::A << Cl::A; // ok
std::cout << &A << &Nm::A; // ok
std::cout << &Cl::A; // linker error: undefined reference to `Cl::A'
Размещение определения Cl::A
в source.cpp (в дополнение к приведенному выше объявлению Cl::A
) устраняет эту ошибку:
// source.cpp
constexpr T Cl::A;
Внешняя связь означает, что всегда будет только один экземпляр Cl::A
. Таким образом, Cl::A
кажется очень хорошим кандидатом на роль большого T
. Однако: можем ли мы быть уверены, что в этом случае фиаско статического порядка инициализации не произойдет? Я считаю, что ответ да, потому что Cl::A
создается во время компиляции.
Я тестировал альтернативы A
, B
, a
с g++ 4.8.2 и 4.9.0, clang++ 3.4 на платформе GNU/Linux. Результаты для трех единиц перевода:
A
в области класса с определением в source.cpp был невосприимчив к фиаско и имел один и тот же адрес во всех единицах трансляции даже во время компиляции.
A
в пространстве имен или глобальной области действия имело 3 разных адреса как для большого массива, так и для constexpr const char * A = "A";
(из-за внутренней связи).
B
(std::array<long double, 100>
) в любой области видимости имел 2 разных адреса (адрес был одинаковым в 2-х единицах трансляции); кроме того, все 3 адреса B
предполагали какое-то другое место в памяти (они были намного больше, чем другие адреса) - я подозреваю, что массив был скопирован в память во время выполнения.
a
при использовании с constexpr
типами T
, т.е. int
, const char *
, std::array
И, инициализированные выражением constexpr
в source.cpp, были так же хороши, как A
: невосприимчивы к фиаско и имели одинаковый адрес во всех единицах трансляции. Если константа типа constexpr
T
инициализируется не-constexpr
, например. std::time(nullptr)
и используется до инициализации, оно будет содержать значение по умолчанию (например, 0
для int
). Это означает, что в данном случае значение константы может зависеть от статического порядка инициализации. Итак, не инициализируйте a
значением, отличным от constexpr
!
Нижняя линия
- в большинстве случаев предпочитайте
A
в области класса для любой константы constexpr, потому что она сочетает в себе идеальную безопасность, простоту, экономию памяти и производительность.
a
(инициализируется значением constexpr
в source.cpp!) следует использовать, если предпочтительна область пространства имен или желательно избегать инициализации в header.hpp (для уменьшения зависимости и время компиляции). a
имеет один недостаток по сравнению с A
: его можно использовать в выражениях времени компиляции только в source.cpp и только после инициализации.
B
следует использовать для небольших T
в некоторых случаях: когда предпочтительна область пространства имен или требуется константа времени компиляции шаблона (например, pi
). Также B
можно использовать, когда значение константы редко используется или используется только в исключительных ситуациях, например. Сообщения об ошибках.
- Other alternatives should almost never be used as they would rarely suit better than all 3 before-mentioned ways.
A
in namespace scope should not be used because it can potentially lead to N instances of constant, hence consume sizeof(T) * N
bytes of memory and cause cache misses. Here N equals to the number of translation units that include header.hpp. As noted in this proposal, A
in namespace scope can violate ODR if used in inline function.
C
можно использовать для больших T
(B
обычно лучше для маленьких T
) в 2 редких случаях: когда предпочтительнее вызов функции; когда предпочтительна область пространства имен И инициализация в заголовке.
D
можно использовать, когда предпочтительнее вызов функции И инициализация в исходном файле.
- Единственный недостаток
C
по сравнению с A
и B
- его возвращаемое значение нельзя использовать в выражении времени компиляции. D
страдает тем же недостатком и еще одним: снижением производительности вызова функции во время выполнения (поскольку ее нельзя встроить).
2
Избегайте использования не-constexpr
a
из-за фиаско статического порядка инициализации. Учитывайте a
только в случае уверенного узкого места. В противном случае безопасность важнее небольшого прироста производительности. b
, c
и d
намного безопаснее. Однако c
и d
имеют 2 требования безопасности:
for (auto f : {
все c
и d
подобные функции}) {
T
конструктор не должен вызывать f
, потому что, если инициализация статической локальной переменной рекурсивно входит в блок, в котором инициализируется переменная, поведение не определено. Обеспечить это не сложно.
- Для каждого класса
X
такого, что X::~X
вызывает f
и существует статически инициализированный объект X
: X::X
должен вызывать f
. Причина в том, что в противном случае static const T
из f
может быть построено после и, следовательно, уничтожено до глобального объекта X
; тогда X::~X
вызовет UB. Это требование гораздо труднее гарантировать, чем предыдущее. Таким образом, он почти запрещает глобальные или статические локальные переменные с сложными деструкторами, использующими глобальные константы. Если деструктор статически инициализированной переменной не сложен, например. использует f()
для ведения журнала, тогда размещение f();
в соответствующем конструкторе обеспечивает безопасность.
}
Примечание: эти 2 требования не относятся к C
и D
:
- рекурсивный вызов
f
не будет компилироваться;
static constexpr T
константы в C
и D
создаются во время компиляции - до создания любой нетривиальной переменной, поэтому они уничтожаются после уничтожения всех нетривиальных переменных (деструкторы вызываются в обратном порядке).
Примечание 2. Часто задаваемые вопросы по C++ предлагает различная реализация c
и d
, которая не накладывает второе требование безопасности. Однако в этом случае статическая константа никогда не уничтожается, что может помешать обнаружению утечки памяти, например. Диагностика Valgrind. Следует избегать утечек памяти, какими бы безобидными они ни были. Так что эти модифицированные версии c
и d
следует использовать только в исключительных ситуациях.
Еще одна альтернатива для рассмотрения здесь — это константа с внутренней связью:
// header.hpp
namespace Ns { namespace { const T a1 = <some value>; } }
Этот подход имеет тот же большой недостаток, что и A
в области пространства имен: внутренняя компоновка может создать столько копий a1
, сколько единиц перевода включает header.hpp. Он также может нарушать ODR так же, как A
. Однако, поскольку другие варианты для не-constexpr
не так хороши, как для констант constexpr
, эта альтернатива на самом деле может иметь редкое применение. НО: это «решение» по-прежнему подвержено фиаско статического порядка инициализации в случае, когда a1
используется в публичной функции, которая, в свою очередь, используется для инициализации глобального объекта. Таким образом, введение внутренней связи не решает проблему — просто скрывает ее, делает ее менее вероятной и, возможно, более трудной для обнаружения и исправления.
Нижняя линия
c
обеспечивает наилучшую производительность и экономит память, поскольку позволяет повторно использовать только один экземпляр T
и может быть встроен, поэтому его следует использовать в большинстве случаев.
d
так же хорош, как c
для экономии памяти, но хуже для производительности, так как он никогда не будет встроенным. Однако d
можно использовать для сокращения времени компиляции.
- рассмотрите
b
для небольших типов или для редко используемых констант (в случае редко используемых констант их определение можно переместить в source.cpp, чтобы избежать перекомпиляции при изменении). Также b
является единственным решением, если требования безопасности для c
и d
не могут быть удовлетворены. b
определенно не подходит для больших T
, если часто используется константа, потому что константа должна создаваться каждый раз, когда вызывается b
.
Примечание. Существует еще одна проблема времени компиляции встроенных функций и переменных, инициализированных в header.hpp. Если определение константы зависит от другой константы, объявленной в другом заголовке bad.h, а заголовок bad.h не должен включаться в header.hpp , затем D
, d
, a
и модифицированный b
(с определением, перемещенным в source.cpp) являются единственными альтернативами.
person
vedg
schedule
15.05.2014
inline
не гарантирует, что функция на самом деле являетсяinline
d компилятором. - person YoungJohn   schedule 16.05.2014extern const
не нарушает ваше требование известности во время компиляции? Как конструкторconstexpr
может быть дорогим? - person aschepler   schedule 16.05.2014extern const std::string str = "my const";
известно во время компиляции. Но он не может быть построен во время компиляции. Конечно, возможно, чтоextern const
на самом деле не является константой, например.extern const std::time_t t = std::time(nullptr);
. В этом случаеt
будет отличаться между запусками. Но я не считаюt
глобальной константой. Итак, в моем вопросе я рассматриваю только подмножество всех возможныхextern const
переменных. - person vedg   schedule 16.05.2014expensive constexpr constructor
: спасибо, я перефразировал эту часть вопроса, а также добавил пространство имен вокруг констант, чтобы не распространять стиль глобальных переменных и заглушать возмущение по поводу такого плохого стиля. - person vedg   schedule 16.05.2014