Принудительное создание шаблона с помощью typedef: успех в g++, сбой в Visual С++

Я хочу форсировать создание шаблона.
Приведенный ниже код работает (выведите 1) на g++ (http://coliru.stacked-crooked.com/a/33986d0e0d320ad4).
Однако в Visual C++ (https://rextester.com/WGQG68063) он печатает неверный результат (0).

#include <iostream>
#include <string>
template <int& T>struct NonTypeParameter { };

//internal implementation
int lala=0;
template <typename T> struct Holder{
    static int init;
};
template <typename T> int Holder<T>::init = lala++;

//tool for user 
template <typename T> struct InitCRTP{ 
    using dummy=NonTypeParameter<Holder<T>::init>;
};

class WantInit : public InitCRTP<WantInit>{};//user register easily
int main(){
    std::cout << lala << std::endl;
}

Это ошибка компилятора Visual C++ или какое-то неопределенное поведение?
Если это ошибка Visual C++, как ее обойти (при этом оставаясь красивой)?

Изменить: Изменить класс → структура, как рекомендовал Макс Лангоф (и многие люди). Поблагодарить.

Щедрость Причина

С противоположными решениями от StoryTeller и Максима Егорушкина и их всесторонним обсуждением (спасибо!) Это звучит как нечеткая область правила C++.

Если это ошибка Visual C++, я хочу, чтобы проблема была достаточно достоверной, чтобы сообщить о ней.

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

Примечание. Я вручил награду Каенбю Рин, потому что для меня это легко понять.
Это не означает, что остальные ответы являются менее правильными или менее полезными.
Я все еще не уверен, что является правильным. Читатели должны действовать с осторожностью.
В целях безопасности я предполагаю, что я просто не могу использовать эту функцию (пока). Всем спасибо.

Ответы

Ответ 1

Конечно, есть ошибка компилятора. Мы можем проверить это, немного изменив InitCRTP:

template <typename T, typename = NonTypeParameter<Holder<T>::init>>
struct InitCRTP {
};

Теперь, ссылаясь на любую специализацию InitCRTP<T>, необходимо использовать Holder<T>::init для определения второго аргумента шаблона. Это, в свою очередь, должно вызвать создание экземпляра Holder<T>::init, и все же VS не создает этого.

В общем, использование класса CRTP в качестве основы должно было бы создать все объявления внутри класса, включая объявление dummy. Так что это тоже должно было сработать.

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

template <typename T> struct InitCRTP{
    using dummy=NonTypeParameter<Holder<T>::init>;
    void dummy2(dummy);
};

Тем не менее, VC++ упрямый. Учитывая все это, и поведение, демонстрируемое как Clang, так и GCC, это ошибка VC++.

Ответ 2

class WantInit : public InitCRTP<WantInit> не создает ни InitCRTP<WantInit>::dummy, ни Holder<WantInit>::init, потому что они не упоминаются чем-то, фактически используемым в программе. Неявная цепочка создания экземпляров в вашем коде не требует создания экземпляров Holder<T>::init, см. неявное создание экземпляров:

Это относится к членам шаблона класса: , если член не используется в программе, он не создан и не требует определения.

Исправление заключается в использовании явной реализации шаблона:

template struct Holder<void>;

Это вызывает создание экземпляра TG45 вместе со всеми его элементами, не являющимися шаблонами.

Кроме того, вы можете создать экземпляр только Holder<T>::init члена, например:

static_cast<void>(Holder<void>::init);

IMO, gcc и clang слишком стремятся создавать экземпляры вещей, на которые нет ссылок. Такое поведение не нарушает и не отклоняет допустимый код, так что это вряд ли ошибка, но в зависимости от такого специфического поведения побочные эффекты являются хрупкими и непереносимыми.

Ответ 3

Давайте попробуем определенно использовать ODR для члена init.

#include <iostream>
#include <string>

int lala=0;
template <typename T> struct Holder{
    static int init;
};
template <typename T> int Holder<T>::init = lala++;

template <typename T> struct InitCRTP{
    InitCRTP() { (void)Holder<T>::init; }
};

class WantInit : public InitCRTP<WantInit>{};
int main(){
    std::cout << lala << std::endl;
    // WantInit w;  <---------------------------- look here
}

Теперь результат программы меняется, если закомментированная строка не закомментирована. Состояние создания шаблона IMHO или состояние использования ODR чего-либо не может зависеть от того, была ли вызвана какая-либо не шаблонная функция (в данном случае конструктор WantInit). Я бы сказал, что есть довольно сильный запах жука.

Ответ 4

Я считаю, что @MaximEgorushkin прав насчет того факта, что dummy на самом деле не создан.

dummy объявлен (потому что это псевдоним типа объявление), и для того, чтобы объявить этот псевдоним, NonTypeParameter<Holder<T>::init> объявлен. Чтобы объявить NonTypeParameter<Holder<T>::init>, должен быть объявлен его шаблонный параметр Holder<T>::init, поэтому также объявлен Holder<T>.

Стандарт требует, чтобы при создании экземпляра класса шаблона были определены его удаленные функции-члены. [temp.spec]

Неявная реализация специализации шаблона класса вызывает: [...]

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

А ссылка на void должна привести к ошибке компиляции.

Мы можем использовать это, чтобы проверить, является ли определенный шаблон специализированным или нет.

#include <iostream>
#include <string>
template <int& T, typename U> struct NonTypeParameter { 
    U& f() = delete;
};

//internal implementation
int lala = 0;
template <typename T> struct Holder {
    T& f() = delete;
    static int init;
};
template <typename T> int Holder<T>::init = lala++;

//tool for user 
template <typename T> struct InitCRTP {
    using dummy = NonTypeParameter<Holder<T>::init, void>;
};

class WantInit : public InitCRTP<WantInit> {};//user register easily
int main() {
    std::cout << lala << std::endl;
}

Этот код компилируется, потому что NonTypeParameter<Holder<T>::init, void> только объявлен, но не создан.

Но если мы изменим class WantInit : public InitCRTP<WantInit> на

class WantInit : public InitCRTP<void>

Не удается скомпилировать в MSVC, g++ и clang.

Это связано с тем, что для объявления NonTypeParameter<Holder<void>::init, void> требуется неявная реализация Holder<void>.

Проблема, с которой столкнулся OP, связана исключительно с незнанием MSVC того, что Holder<T>::init используется для ODR:

#include <iostream>

template <int& T> struct NonTypeParameter { };

int lala = 0;

template <typename T> struct Holder {
    static int init;
};

template <typename T> int Holder<T>::init = lala++;

int main() {
    NonTypeParameter<Holder<int>::init> odr;
    std::cout << lala << std::endl;
}

MSVC выдаст 0. Это означает, что он не понимает, что Holder<int>::init использовался ODR.

Компилятор Explorer Link