Глобальные константы в C++11

Каковы наилучшие способы объявления и определения глобальных констант в C++? Меня больше всего интересует стандарт С++ 11, так как он многое исправляет в этом отношении.

[EDIT (уточнение)]: в этом вопросе «глобальная константа» означает постоянную переменную или функцию, которая известна во время компиляции в любой области. Глобальная константа должна быть доступна более чем из одной единицы перевода. Это не обязательно константа в стиле constexpr — может быть что-то вроде const std::map<int, std::string> m = { { 1, "U" }, { 5, "V" } }; или const std::map<int, std::string> * mAddr() { return & m; }. В этом вопросе я не касаюсь предпочтительной области применения или названия константы в хорошем стиле. Оставим эти вопросы для другого вопроса. [END_EDIT]

Я хочу знать ответы для всех разных случаев, поэтому предположим, что T является одним из следующих:

typedef    int                     T;  // 1
typedef    long double             T;  // 2
typedef    std::array<char, 1>     T;  // 3
typedef    std::array<long, 1000>  T;  // 4
typedef    std::string             T;  // 5
typedef    QString                 T;  // 6
class      T {
   // unspecified amount of code
};                                     // 7
// Something special
// not mentioned above?                // 8

Я считаю, что нет большой семантической (здесь я не обсуждаю хорошее название или стиль области действия) разницы между тремя возможными областями действия:

// header.hpp
extern const T tv;
T tf();                  // Global
namespace Nm {
    extern const T tv;
    T tf();              // Namespace
}
struct Cl {
    static const T tv;
    static T tf();       // Class
};

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

Рассмотрим также случай, когда вызов функции используется в определении константы, например. <some value>==f();. Как вызов функции в константной инициализации повлияет на выбор между альтернативами?

  1. Давайте сначала рассмотрим T с конструктором constexpr. Очевидными альтернативами являются:

    // header.hpp
    namespace Ns {
    constexpr T A = <some value>;
    constexpr T B() { return <some value>; }
    inline const T & C() { static constexpr T t = <some value>; return t; }
    const T & D();
    }
    
    // source.cpp
    const T & Ns::D() { static constexpr T t = <some value>; return t; }
    

    Я считаю, что A и B больше всего подходят для небольших T (таких, что наличие нескольких экземпляров или их копирование во время выполнения не является проблемой), например. 1-3, иногда 7. C и D лучше, если T большое, т.е. 4, иногда 7.

  2. T без конструктора constexpr. Альтернативы:

    // header.hpp
    namespace Ns {
    extern const T a;
    inline T b() { return <some value>; }
    inline const T & c() { static const T t = <some value>; return t; }
    const T & d();
    }
    
    // source.cpp
    extern const T Ns::a = <some value>;
    const T & Ns::d() { static const T t = <some value>; return t; }
    

    Обычно я бы не использовал a из-за фиаско статического порядка инициализации. Насколько я знаю, b, c и d совершенно безопасны, даже потокобезопасны, начиная с C++11. b не кажется хорошим выбором, если только T не имеет очень дешевого конструктора, что необычно для конструкторов, отличных от constexpr. Я могу назвать одно преимущество c перед d - отсутствие вызова функции (производительность во время выполнения); одно преимущество d перед c - меньше перекомпиляции при изменении значения константы (эти преимущества также относятся к C и D). Я уверен, что я упустил много рассуждений здесь. Предоставьте другие соображения в ответах, пожалуйста.

Если вы хотите изменить/протестировать приведенный выше код, вы можете использовать мои тестовые файлы (только header.hpp, source.cpp с компилируемыми версиями приведенных выше фрагментов кода и main.cpp, который печатает константы из header.hpp): https://docs.google.com/uc?export=download&id=0B0F-aqLyFk_PVUTSRnZWWnd4Tjg


person vedg    schedule 14.05.2014    source источник
comment
У нас есть терминология для обсуждения этих вещей: область действия, время жизни, класс хранения, связь. Предлагаю вам ознакомиться с их значениями, вы наверняка узнаете то, что хотели узнать из их определений, а если нет, то можете отредактировать свой вопрос с нужной терминологией, чтобы стало понятно.   -  person Ben Voigt    schedule 14.05.2014
comment
Я думаю, что «область действия», «срок службы» и «класс хранения» имеют лишь незначительное отношение к вопросу. Прочитал про «связку», узнал кое-что новое, но пока не вижу ничего плохого в предложенных альтернативах. Я согласен, что мой вопрос не очень ясен, но не из-за неиспользованной терминологии, а потому, что я недостаточно хорошо объяснил упомянутые преимущества. Я просто чувствую, что мой вопрос уже достаточно длинный. Кажется, мне придется сделать его еще более подробным, если никто не даст хорошего ответа в ближайшем будущем.   -  person vedg    schedule 14.05.2014
comment
имейте в виду, что inline не гарантирует, что функция на самом деле является inlined компилятором.   -  person YoungJohn    schedule 16.05.2014
comment
Разве extern const не нарушает ваше требование известности во время компиляции? Как конструктор constexpr может быть дорогим?   -  person aschepler    schedule 16.05.2014
comment
@YoungJohn, да, не всегда. Но я верю, что современный компилятор в этом случае поступит правильно. Так что, если функция не будет встроена, то, вероятно, так будет лучше.   -  person vedg    schedule 16.05.2014
comment
@aschepler, я бы сказал, что extern 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.2014
comment
Что касается expensive constexpr constructor: спасибо, я перефразировал эту часть вопроса, а также добавил пространство имен вокруг констант, чтобы не распространять стиль глобальных переменных и заглушать возмущение по поводу такого плохого стиля.   -  person vedg    schedule 16.05.2014


Ответы (3)


Я считаю, что нет большой разницы между следующими местами объявления:

Это неправильно во многих отношениях.

Первое объявление загрязняет глобальное пространство имен; вы убрали название "tv" от того, чтобы оно никогда больше не использовалось, без возможности недопонимания. Это может вызвать теневые предупреждения, это может привести к ошибкам компоновщика, это может привести к разного рода путанице у любого, кто использует ваш заголовок. Это также может вызвать проблемы у того, кто не использует ваш заголовок, вызывая коллизию с кем-то еще, кто также использует ваше имя переменной как глобальную.

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

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

Это более современный стиль C++03, а C++11 значительно расширяет эту тактику за счет переименования шаблонов.

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

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

Давайте теперь посмотрим на ваши дела.

  1. Размер конструктора, если он возвращает константное выражение, неважен; весь код должен быть исполняемым во время компиляции. Это означает, что сложность не имеет смысла; он всегда будет компилироваться в одно постоянное возвращаемое значение. Вы почти наверняка никогда не должны использовать C или D; все, что нужно сделать, это сделать так, чтобы оптимизация constexpr не работала. Я бы использовал тот из A и B, который выглядит более элегантно, вероятно, простым присваиванием будет A, а сложным постоянным выражением будет B.

  2. Ни один из них не обязательно является потокобезопасным; содержимое конструктора будет определять как безопасность потоков, так и безопасность исключений, и довольно легко сделать любой из этих операторов небезопасным для потоков. На самом деле A, скорее всего, будет потокобезопасным; пока к объекту не обращаются до вызова main, он должен быть полностью сформирован; этого нельзя сказать ни о каком другом вашем примере. Что касается вашего анализа B, по моему опыту, большинство конструкторов (особенно безопасных для исключений) дешевы, поскольку они избегают распределения. В таких случаях вряд ли будет большая разница между любым из ваших случаев.

Я настоятельно рекомендую вам прекратить подобные попытки микрооптимизации и, возможно, лучше понять идиомы C++. Большинство вещей, которые вы пытаетесь здесь сделать, вряд ли приведут к увеличению производительности.

person Alice    schedule 14.05.2014
comment
О местах объявления: да, я знал об этих различиях в области видимости, но спросил, влияют ли они на правила использования констант. - person vedg; 14.05.2014
comment
1. Объект constexpr не всегда не является переменной, он может ссылаться на местоположение в памяти (см. смысл" title="имеет ли смысл статическая переменная constexpr"> stackoverflow.com/questions/13865842/). Можно взять адрес объекта constexpr. Учитывая это, между A и B есть разница: A имеет внутреннюю связь, поэтому может быть построена в каждой единице перевода, включающей header.hpp. Это не очень хорошо для больших объектов, таких как 4. - person vedg; 14.05.2014
comment
Боюсь, что в случае с B тоже проблемы с большими объектами: а вдруг нужно передать const T & в какую-то функцию? constexpr T t = B(); f(t); - будет ли T копироваться здесь во время выполнения? Вот почему я предложил C и D для дорогостоящих объектов. - person vedg; 14.05.2014
comment
2. b, c и d являются потокобезопасными, поскольку C++11 для разумно написанных конструкторов T, которые не вызывают одну из этих функций (b, c, d). Это не сложно гарантировать. Кроме этого, я не вижу ничего опасного в этих альтернативах a. Статические локальные переменные хорошо описаны здесь: en.cppreference.com/w/cpp/language/ storage_duration (обратите внимание на раздел с C++11). - person vedg; 14.05.2014
comment
Этот вопрос в первую очередь касается безопасных констант. После прочтения вашего ответа кажется, что a - это правильный путь, но часто это небезопасно и опасно (parashift.com/c++-faq/static-init-order.html). После того, как о безопасности позаботятся, хорошо взвесить производительность во время компиляции и выполнения. Поскольку глобальные константы используются повсеместно, разумно сформировать определенные рекомендации — где использовать каждую форму. Я не смог найти удовлетворительных рекомендаций и задал этот вопрос. Итак, мой вопрос касается именно идиом С++. - person vedg; 14.05.2014
comment
2. b, c и d НЕ являются потокобезопасными; нет абсолютно ничего, что запрещало бы вам писать не потокобезопасный конструктор, используя эту идиому. На самом деле очень легко написать конструктор, не являющийся потокобезопасным, используя C и D; это одна из проблем с ленивыми синглтонами, использующими эти идиомы. Вы говорите разумно, хорошо написано, как будто это что-то значит; либо метод является потокобезопасным, либо нет, либо он не дает никаких гарантий. В этих случаях гарантии нет. - person Alice; 15.05.2014
comment
1. Вы неправильно понимаете смысл возможности получить адрес объекта постоянного выражения; это не обязательно относится к расположению памяти (компилятор может оптимизировать эту косвенность и может сделать это во многих случаях). Если объект постоянного выражения не оптимизируется во время компиляции, возможно, вы делаете что-то не так. Объект постоянного выражения не должен иметь значения, насколько он дорог, потому что его следует оптимизировать. - person Alice; 15.05.2014
comment
3. А не является небезопасным или опасным; это опасно только в том случае, если вам нужен определенный порядок, и в этом случае вы должны использовать глобальный объект-оболочку для обеспечения соблюдения этого порядка. Это не так опасно, как небезопасны malloc/free; если вы используете что-то неправильно или зависите от ограничений, которых не существует, виноваты вы, а не язык. - person Alice; 15.05.2014
comment
Наконец, я не согласен с тем, что глобальные константы обычно используются; это не совсем верно и часто свидетельствует о плохом дизайне. Как правило, я видел, как люди используют синглетоны, а не глобальные переменные (хотя синглтоны также могут создавать свои собственные проблемы). Лучший дизайн состоит в том, чтобы максимально избегать глобального состояния, независимо от того, насколько хорошо оно скрыто. - person Alice; 15.05.2014
comment
Я считаю, что конструктор либо тривиален, либо достаточно хорошо написан, чтобы быть потокобезопасным в данном случае для 1-6. И нетрудно написать соответствующий конструктор для пользовательского класса. Потому что, если этот конструктор не делает что-то особенное, например, доступ к глобальным переменным, статическая константа в функции будет безопасной в соответствии со стандартом C++11. 1. Я все еще очень сомневаюсь, что такой объект, как 4, всегда будет оптимизирован, но у меня нет доказательств этого; с другой стороны, вы также не предоставили никаких доказательств или ссылок в поддержку своей позиции. - person vedg; 15.05.2014
comment
Я предпочитаю не думать о возможных проблемах со статическим порядком инициализации каждый раз, когда я меняю свой код. Функции доступа обеспечивают простое и дешевое (либо во время компиляции, либо во время выполнения, но не то и другое) решение. Функции также позволяют избежать других проблем — медленного запуска приложения, инициализации неиспользуемых переменных. - person vedg; 15.05.2014
comment
Под «глобальными константами» я подразумеваю константу (как в A и a) или одну из предложенных альтернативных функций. Кроме того, как указано в моем вопросе, я не вижу разницы, относящейся к вопросу, между глобальным пространством имен, именованным пространством имен и областью действия класса. Несмотря на то, что все мои альтернативы представляют собой простые глобальные переменные/функции, их можно (и обычно нужно) перемещать в пространство имен или область видимости класса. Считаете ли вы, что глобальная константа в любой форме — плохой замысел? Как насчет std::numeric_limits, pi (шаблон пользовательской функции)? - person vedg; 15.05.2014
comment
Я не понимаю вашего вопроса; numeric_limits не является глобальным. Это шаблонный класс в стандартном пространстве имен. Никакая его часть не является глобальной; он ограничен как пространством имен, так и внутри класса, а также в области создания экземпляра; это три уровня косвенности от чего-либо, похожего на глобальный. Кроме того, большая часть этого не переменные, а методы, еще один уровень косвенности. Кроме того, да, любое глобальное состояние усложняет анализ программ. Как и при использовании goto, глобальное состояние нужно свести к минимуму. - person Alice; 15.05.2014
comment
Если вы не видите актуальности, то я не знаю, как вам это объяснить; в C++ мы пытаемся максимально инкапсулировать и скрыть внутренние детали. Ваш вопрос касался идиом С++; если вы не видите разницы между утверждениями, то вы не понимаете современный C++. - person Alice; 15.05.2014
comment
В этом вопросе под глобальной константой я понимаю постоянную переменную или функцию, которая известна во время компиляции в любой области. Мои альтернативы - это просто примеры, а не хорошие рекомендации по объему. То, что вы пишете о хорошем стиле с областями действия, безусловно, правильно, но я пытался обобщить рекомендации, связанные с константами, независимо от области видимости. Поэтому я предлагаю вам больше не упоминать область действия, если только это не повлияет на выбор альтернативы (переменной или функции). Я также прошу вас не оспаривать мои короткие имена переменных/функций — я не утверждаю, что такие короткие имена представляют собой хороший стиль именования :) - person vedg; 15.05.2014
comment
Хороший ответ, за исключением одной ошибки: class = struct в C++ (за исключением члена по умолчанию и базовой видимости). Вы даже можете предварительно объявить struct, а затем определить его как class (и наоборот). - person Konrad Rudolph; 15.05.2014
comment
@KonradRudolph Верно, это различия между структурой и классом, которые я специально считаю важными. - person Alice; 08.11.2014

Вы не упомянули важную опцию:

namespace
{
    const T t = .....;
};

Теперь нет проблем с конфликтами имен.

Это не подходит, если T — это то, что вы хотите построить только один раз. Но наличие большого "глобального" объекта, константного или нет, - это то, чего вы действительно хотите избежать. Он нарушает инкапсуляцию, а также вводит фиаско статического порядка инициализации в ваш код.

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

В моем коде, который, кажется, вызывает большой неконстантный глобальный объект, на самом деле у меня есть функция,

namespace MyStuff
{
     T &get_global_T();
}

который создает объект при первом использовании. (На самом деле сам объект скрыт в одном модуле, а T — это вспомогательный класс, определяющий интерфейс, поэтому я могу возиться с деталями объекта и не мешать коду, который его использует).

person M.M    schedule 15.05.2014
comment
Да, безымянное пространство имен в заголовке — интересная идея. Это почти то же самое (но предпочтительнее) static const T t = ...; в области пространства имен и обеспечивает внутреннюю связь. Я определенно предпочитаю внешнюю связь в случае объектов constexpr. Не уверен в простом const... Я полагаю, что этот способ может конкурировать с b, если требования безопасности для c и d не могут быть удовлетворены, но T большой и используется часто. - person vedg; 16.05.2014
comment
В противном случае я не вижу особого смысла в этой альтернативе, потому что внутренняя компоновка приведет к тому, что будет столько копий константы, сколько единиц перевода, включающих header.hpp. таблица является локальной для модуля с реализацией этой функции - она ​​не спасет вас от фиаско статического порядка инициализации, если эта таблица объявлена ​​как const Table table=...; и эта функция вызывается из конструктора или деструктора глобального объекта. - person vedg; 16.05.2014
comment
столько копий константы - ее можно оптимизировать, а даже если нет - ну и что? - person M.M; 17.05.2014
comment
Если это не так, это приведет к пустой трате памяти, но это не моя главная забота. Боюсь, что большое количество копий также может привести к проблемам с производительностью: будет несколько одинаковых констант, и они будут заменяться друг другом в кеше процессора. - person vedg; 17.05.2014
comment
Кроме того, переменные в безымянном пространстве имен потенциально могут нарушать ODR при использовании во встроенных функциях. См., например, эту статью. - person vedg; 29.04.2015

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!

Нижняя линия

  1. в большинстве случаев предпочитайте A в области класса для любой константы constexpr, потому что она сочетает в себе идеальную безопасность, простоту, экономию памяти и производительность.
  2. a (инициализируется значением constexpr в source.cpp!) следует использовать, если предпочтительна область пространства имен или желательно избегать инициализации в header.hpp (для уменьшения зависимости и время компиляции). a имеет один недостаток по сравнению с A: его можно использовать в выражениях времени компиляции только в source.cpp и только после инициализации.
  3. B следует использовать для небольших T в некоторых случаях: когда предпочтительна область пространства имен или требуется константа времени компиляции шаблона (например, pi). Также B можно использовать, когда значение константы редко используется или используется только в исключительных ситуациях, например. Сообщения об ошибках.
  4. 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
comment
Еще одно существенное отличие c/d от других версий, отличных от constexpr, заключается в том, что инициализация выполняется лениво. Только при первом вызове c или d будет найдено значение для инициализации t. Это очень важно, если <some value> не является константой времени компиляции, а скорее чем-то, что требует вычислений во время выполнения или имеет побочные эффекты, или то же самое относится к конструктору T. Легко представить себе библиотеку, в которой пользователи никогда не смогут вызывать все предоставленные f. Единовременная ленивая инициализация также является потокобезопасной, что не является тривиальным для написания кода с нуля. - person TrentP; 19.07.2017
comment
1. или с побочными эффектами — я бы значительно избегал побочных эффектов в глобальной константе. 2. Да, ситуация, когда f не вызывается, идеальна с точки зрения производительности. - person vedg; 19.07.2017
comment
3. Я стал не любить статические переменные в функциях с тех пор, как написал этот ответ. Потокобезопасность связана с затратами на производительность при каждом вызове функции (я думаю, блокировка мьютекса). В настоящее время я бы предпочел передавать данные контекста, созданные во время инициализации приложения/библиотеки, в конструкторы/функции вместо того, чтобы static хранить их в функциях. Это также может облегчить понимание и изменение логики программы. Например, можно считывать константы из конфигурационного файла в объект контекста во время инициализации вместо жесткого кодирования констант в коде программы. - person vedg; 19.07.2017
comment
4. Если я правильно понимаю, встроенные переменные С++ 17 очень похожи на встроенные функции и могут нести те же затраты производительности, что и статические переменные в функциях. - person vedg; 19.07.2017