Вот мой код:
#include <iostream>
class MyBaseClass
{
public:
static int StaticInt;
};
int MyBaseClass::StaticInt = 0;
template <int N> class MyClassT : public MyBaseClass
{
public:
MyClassT()
{
StaticInt = N;
};
};
template <int N> static MyClassT<N> AnchorObjT = {};
class UserClass
{
friend void fn()
{
std::cout << "in fn()" << std::endl; //this never runs
(void)AnchorObjT<123>;
};
};
int main()
{
std::cout << MyBaseClass::StaticInt << std::endl;
return 0;
}
Выход:
123
…указывающий MyClassT()
конструктор был вызван, несмотря на это fn()
никогда не звонил.
Проверено на gcc
а также clang
с -O0
, -O3
, -Os
и даже -Ofast
Эта программа имеет неопределенное поведение в соответствии со стандартом C ++?
Другими словами: если более поздние версии компиляторов обнаружат, что fn()
никогда не будет вызван, могут ли они оптимизировать создание шаблона вместе с запуском конструктора?
Можно ли как-то сделать этот код детерминированным, то есть заставить конструктор работать — без ссылка на имя функции fn
или значение параметра шаблона 123
за пределами UserClass
?
ОБНОВИТЬ: Модератор усек мой вопрос и предложил дальнейшее усечение. Оригинальную подробную версию можно посмотреть Вот.
Создание экземпляра шаблона является функцией кода, а не функцией каких-либо динамических условий выполнения. В качестве упрощенного примера:
template <typename T> void bar();
void foo(bool b) {
if (b) {
bar<int>();
} else {
bar<double>();
}
}
И то и другое bar<int>
а также bar<double>
создаются здесь, даже если foo
никогда не вызывается или даже если foo
вызывается только с true
,
Для переменной шаблона, в частности, правило [Temp.inst] / 6:
Если специализация шаблона переменной не была явно создана или явно специализирована, специализация шаблона переменной создается неявно, когда на нее ссылаются в контексте, для которого требуется определение переменной, или если существование определения влияет на семантику программы.
В вашей функции:
friend void fn() { (void)AnchorObjT<123>; };
AnchorObjT<123>
ссылка в контексте, который требует определения (независимо от того, fn()
когда-либо вызывается или даже, в этом случае, даже можно позвонить), следовательно, он создается.
Но AnchorObjT<123>
является глобальной переменной, поэтому ее создание означает, что у нас есть объект, созданный ранее main()
— к тому времени, когда мы входим main()
, AnchorObjT<123>
конструктор будет запущен, настройка StaticInt
в 123
, Обратите внимание, что нам не нужно на самом деле запустить fn()
чтобы вызвать этот конструктор — fn()
Роль здесь в том, чтобы просто создать экземпляр шаблона переменной, его конструктор вызывается в другом месте.
Печать 123 — правильное, ожидаемое поведение.
Обратите внимание, что в то время как язык требует глобального объекта AnchorObjT<123>
чтобы существовать, компоновщик может все еще объект, потому что нет ссылки на него. Предполагая, что ваша реальная программа делает больше с этим объектом, если вам нужно, чтобы он существовал, вам может потребоваться сделать с ним больше, чтобы не допустить его удаления компоновщиком (например, gcc имеет used
атрибут).
«Если более поздним версиям компиляторов удастся обнаружить, что fn () никогда не будет вызываться [и] они оптимизируют удаление шаблона», то эти компиляторы будут сломаны.
Компиляторы C ++ могут свободно реализовывать любую оптимизацию это не имеет заметного эффекта. В описанной вами ситуации будет, по крайней мере, один наблюдаемый эффект: а именно, член статического класса не создается и не инициализируется, поэтому компилятор C ++ не может полностью его оптимизировать. Этого не произойдет.
Компилятор может игнорировать все остальное в вызове функции и фактически не компилировать сам вызов функции, но компилятор должен делать все, что ему нужно, для организации мероприятий, чтобы статический член класса был инициализирован как будто этот вызов функции был сделан.
Если компилятор может определить, что ничего другого в программе фактически не использует член статического класса, и полное его удаление не имеет наблюдаемого эффекта, то компилятор может удалить член статического класса и функцию, которая его инициализирует (поскольку ничто другое не ссылается на функцию ).
Обратите внимание, что даже получение адреса функции (или члена класса) приведет к наблюдаемому эффекту, поэтому, даже если на самом деле функция не вызывает ничего, но что-то берет адрес функции, она не может просто исчезнуть.
Постскриптум — все вышеперечисленное предполагает отсутствие неопределенного поведения в коде C ++. При неопределенном поведении, входящем в изображение, все правила выходят в окно.
Короткий ответ — это работает.
Длинный ответ — это работает если компоновщик отбрасывает всю вашу единицу перевода (.obj).
Это может произойти, когда вы создаете .lib и связываете его. Компоновщик обычно выбирает, какой .obj связать из библиотеки, на основе графа зависимостей «использую ли я символы, которые экспортирует obj».
Так что если вы используете эту технику в файле cpp, то в файлах cpp нет символов, которые используются в другом месте вашего exexutable (в том числе косвенно через другой объект obj в вашей библиотеке, который, в свою очередь, используется исполняемым файлом), компоновщик может отбросить файл obj yoir ,
Я испытал это с лязгом. Мы где создавали саморегистрационные фабрики, а некоторые где сбрасывали. Чтобы исправить это, мы создали несколько макросов, которые вызывали существование тривиальной зависимости, предотвращая удаление файла obj.
Это не противоречит другим ответам, потому что процесс связывания библиотеки не требует решения, что есть, а что нет в вашей программе.