Подтвердить что ты не робот

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

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

[EDIT (уточнение)]: в этом вопросе "глобальная константа" обозначает постоянную переменную или функцию, которая известна во время компиляции в любой области. Глобальная константа должна быть доступна из нескольких единиц перевода. Это не обязательно константа constexpr-style - может быть что-то вроде 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();. Как вызов функции при постоянной инициализации влияет на выбор между альтернативами?

  • Рассмотрим сначала конструкцию 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.

  • 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 являются абсолютно безопасными, даже потокобезопасными с С++ 11. B, по-видимому, не является хорошим выбором, если T не имеет очень дешевого конструктора, что необычно для конструкторов без constexpr. Я могу назвать одно преимущество C over D - отсутствие вызова функции (время выполнения); одно преимущество D over C - меньше перекомпиляции при изменении постоянного значения (эти преимущества также применимы к C и D). Я уверен, что я пропустил много рассуждений. Предоставьте другие соображения в ответах.

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Теперь посмотрим на ваши случаи.

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

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

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

Ответ 2

Вы не указали важный параметр:

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

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

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

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

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

namespace MyStuff
{
     T &get_global_T();
}

который создает объект при первом использовании. (Фактически сам объект скрыт в одной единице, а T - вспомогательный класс, который указывает интерфейс, поэтому я могу обходиться с деталями объекта и не нарушать какой-либо код, который его использует).

Ответ 3

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 в классе scope с определением в 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 type T инициализируется с помощью не constexpr, например. std::time(nullptr) и используется до инициализации, он будет содержать значение по умолчанию (например, 0 для int). Это означает, что в этом случае постоянное значение может зависеть от статического порядка инициализации. Итак, не инициализируйте A значением <<2 →

Нижняя строка

  • предпочитает A в области класса для любой константы constexpr в большинстве случаев, потому что она сочетает в себе отличную безопасность, простоту, экономию памяти и производительность.
  • A (инициализируется значением constexpr в source.cpp!), следует использовать, если область пространства имен предпочтительнее или желательно избежать инициализации в header.hpp(чтобы уменьшить зависимости и время компиляции). A имеет один недостаток по сравнению с A: его можно использовать в выражениях времени компиляции только в source.cpp и только после инициализации.
  • B следует использовать для небольших T в некоторых случаях: когда область пространства имен предпочтительнее или необходима константа времени компиляции шаблона (например, pi). Кроме того, B может использоваться, когда постоянное значение редко используется или используется только в исключительных ситуациях, например. сообщения об ошибках.
  • Другие альтернативы почти никогда не должны использоваться, поскольку они редко подходят лучше всех трех вышеупомянутых способов.
    • A в области пространства имен не следует использовать, поскольку он потенциально может привести к N экземплярам константы, поэтому потребляет sizeof(T) * N байт памяти и вызывает промахи в кэше. Здесь N равно числу единиц перевода, которые включают header.hpp. Как указано в это предложение, A в области пространства имен может нарушать ODR, если используется в встроенной функции.
    • C может использоваться для больших T (B обычно лучше для малых T) в двух редких сценариях: когда вызов функции предпочтительнее; когда область пространства имен И инициализация в заголовке предпочтительнее.
    • D может использоваться, когда предпочтительным является вызов функции И инициализация в исходном файле.
    • Единственный недостаток C по сравнению с A и B - его возвращаемое значение не может использоваться во время выражения компиляции. D страдает от одного и того же недостатка, а другой: отбой производительности исполнения вызова функции (потому что он не может быть встроен).

2

Избегайте использования не constexpr A из-за фиаско порядка статического инициализации. Рассмотрим A только в случае уверенного узкого места. В противном случае безопасность важнее, чем небольшой прирост производительности. B, C и D намного безопаснее. Однако C и D имеют 2 требования безопасности:

for (auto f : { все C и D -подобные функции }) {

  • Конструктор
  • T не должен вызывать f, потому что если инициализация статической локальной переменной рекурсивно входит в блок, в котором инициализируется переменная, поведение undefined. Это не сложно.
  • Для каждого класса 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: С++ FAQ предлагает другую реализацию 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) являются единственными альтернативами.