clang и gcc по-разному ведут себя при обработке создания шаблонов и статических членов constexpr?

Рассмотрим следующую программу (извините за длину, это самый короткий способ, который я мог придумать, чтобы выразить проблему):

#include <iostream>
#include <vector>
#include <typeindex>

using namespace std;

std::vector<std::type_index>&
test_vector()
{
  static std::vector<std::type_index> rv;
  return rv;
}

template <typename T>
class RegistrarWrapper;

template<typename T>
class Registrar
{
  Registrar()
  {
    auto& test_vect = test_vector();
    test_vect.push_back(std::type_index(typeid(T)));
  }
  friend class RegistrarWrapper<T>;
};

template <typename T>
class RegistrarWrapper
{
  public:
    static Registrar<T> registrar;
    typedef Registrar<T> registrar_t;
};

template <typename T>
Registrar<T> RegistrarWrapper<T>::registrar;


template <typename T>
class Foo
{
  public:
    // Refer to the static registrar somewhere to make the compiler
    // generate it ?!?!?!?
    static constexpr typename RegistrarWrapper<Foo<T>>::registrar_t& __reg_ptr =
      RegistrarWrapper<Foo<T>>::registrar;
};


int main(int argc, char** argv)
{
  Foo<int> a;
  Foo<bool> b;
  Foo<std::string> c;

  for(auto&& data : test_vector()) {
    std::cout << data.name() << std::endl;
  }

}

При компиляции с clang++ (версия 3.5.2, разумеется, с -std=c++11) эта программа выводит (для удобочитаемости передается через c++filt):

Foo<int>
Foo<bool>
Foo<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >

А вот с g++ (пробовал версии 4.8.5, 4.9.3 и 5.2.0) ничего не выводит! Что тут происходит? Какой компилятор соответствует стандарту С++? Как я могу создать этот эффект независимо от компилятора (предпочтительно без каких-либо накладных расходов во время выполнения)?


person Daisy Sophia Hollman    schedule 03.11.2015    source источник
comment
… и вы действительно не могли еще больше упростить тестовый пример?   -  person Columbo    schedule 03.11.2015
comment
Я запустил ваш код с помощью clang++ 3.7.0, и он компилируется чисто, за исключением предупреждения о неиспользуемых переменных.   -  person Amadeus    schedule 03.11.2015
comment
@Amadeus Он чисто компилируется как в clang++, так и в g++. Проблема в том, что выход разный.   -  person Daisy Sophia Hollman    schedule 03.11.2015
comment
@Columbo, я открыт для предложений   -  person Daisy Sophia Hollman    schedule 03.11.2015
comment
FWIW, он компилируется и запускается с VS2015 (даже если тип std::string немного отличается).   -  person Bo Persson    schedule 03.11.2015
comment
@BoPersson Спасибо. У меня нет доступа к большому количеству других компиляторов, и мне интересно посмотреть, является ли clang или gcc лишними здесь.   -  person Daisy Sophia Hollman    schedule 03.11.2015
comment
Foo() { (void)&RegistrarWrapper<Foo<T>>::registrar; } (и избавление от члена constexpr) решает эту проблему во всех трех компиляторах и не требует дополнительных затрат времени выполнения по сравнению с вашим исходным решением. Еще несколько идей здесь: stackoverflow.com/a/27965993/4326278.   -  person bogdan    schedule 03.11.2015
comment
@bogdan Конечно, конечно, строка об инициализации и использовании статических элементов данных находится в предложении «Шаблоны» ... Я потратил около 30 минут на ее поиски и сдался. Вздох.   -  person Barry    schedule 03.11.2015
comment
@DavidHollman Ответ, который вы ищете, представляет собой комбинацию принятого ответа на связанный вопрос (как это сделать) и второго ответа (почему gcc/clang дают разные ответы)   -  person Barry    schedule 03.11.2015
comment
@bogdan Но это исключает, например, возможность обрабатывать Foo как обычные старые данные, поскольку теперь у него есть определяемый пользователем конструктор по умолчанию.   -  person Daisy Sophia Hollman    schedule 03.11.2015
comment
Верно, но вы не упомянули это как требование в своем вопросе. Я думаю, что требование тривиального конструктора отличает вопрос от связанного. @ Барри, что ты думаешь?   -  person bogdan    schedule 03.11.2015
comment
... и в этом случае я бы выбрал static char reg[register_class<Foo<T>>()];, где register_class — это шаблон функции constexpr, который выполняет танец (void)&RegistrarWrapper<T>::registrar; в своем теле и возвращает, скажем, 1.   -  person bogdan    schedule 03.11.2015
comment
@bogdan Да, это сработает. Открыто.   -  person Barry    schedule 04.11.2015
comment
@bogdan это работает, спасибо   -  person Daisy Sophia Hollman    schedule 04.11.2015


Ответы (1)


Сначала пара решений. Для обоих из них важной частью является получение адреса registrar из кода, который гарантированно будет создан. Это гарантирует, что определение статического члена также будет создано, вызывая побочный эффект.

Первый основан на том факте, что определение конструктора по умолчанию для каждой специализации Foo создается для обработки инициализации по умолчанию a, b и c в main:

template<typename T> class Foo
{
public:
   Foo() { (void)&RegistrarWrapper<Foo<T>>::registrar; }
};

Недостатком является то, что это вводит нетривиальный конструктор. Альтернативой, позволяющей избежать этой проблемы, является следующее:

template<class T> constexpr std::size_t register_class() 
{ 
   (void)&RegistrarWrapper<T>::registrar; 
   return 1; 
}

template<typename T> class Foo
{
   static char reg[register_class<Foo<T>>()];
};

Ключевым моментом здесь является запуск создания экземпляра в объявлении статического члена, не полагаясь на какой-либо инициализатор (см. ниже).

Оба решения отлично работают в Clang 3.7.0, GCC 5.2.0 и Visual C++ 2015 как с включенной оптимизацией, так и без нее. Второй использует расширенные правила для функций constexpr, которые являются функцией C++14. Конечно, есть несколько простых способов сделать его совместимым с C++11, если это необходимо.


Я думаю, что проблема с вашим решением заключается в том, что нет гарантии, что инициализатор для __reg_ptr будет создан, если его значение где-то не используется. Некоторые стандартные цитаты из N4527:

14.7.1p2:

[...] инициализация (и любые связанные с ней побочные эффекты) статического члена данных не происходит, если сам статический член данных не используется таким образом, который требует определения статического члена данных для существования.

Это не совсем относится к случаю constexpr, поскольку (я думаю) речь идет о внеклассовом определении статического члена данных, который используется odr (это больше относится к registrar), но это близко.

14.7.1p1:

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

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

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


Третий вариант: способ заставить ваше решение работать — это явно создавать экземпляры специализаций Foo вместо того, чтобы полагаться на неявные экземпляры:

template class Foo<int>;
template class Foo<bool>;
template class Foo<std::string>;

int main()
{
   for(auto&& data : test_vector()) {
      std::cout << data.name() << std::endl;
   }
}

Это также работает во всех трех компиляторах и зависит от 14.7.2p8:

Явное создание экземпляра, которое называет специализацию шаблона класса, также является явным созданием экземпляра того же типа (объявление или определение) каждого из его членов [...]

Учитывая, что это явные определения создания экземпляров, кажется, этого достаточно, чтобы убедить GCC создать экземпляр инициализатора для __reg_ptr. Однако эти явные определения инстанцирования могут появляться во всей программе только один раз ([14.7p5.1]), поэтому требуется дополнительная осторожность. Я считаю первые два решения более надежными.

person bogdan    schedule 03.11.2015