clang 和 gcc 在处理模板生成和静态 constexpr 成员时的不同行为?

clang and gcc different behavior when handling template generation and static constexpr members?

考虑以下程序(对长度感到抱歉;这是我能想到的表达问题的最短方式):

#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 版本),它没有任何输出!这里发生了什么?哪个编译器符合c++标准?我如何以与编译器无关的方式创建这种效果(最好没有任何 运行 时间开销)?

首先,几个解决方案。对于它们两者,重要的部分是从保证实例化的代码中获取 registrar 的地址。这确保静态成员的定义也被实例化,触发副作用。

第一个依赖于Foo的每个特化的默认构造函数的定义被实例化以处理ab和[=的默认初始化17=] 在 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:

[...] the initialization (and any associated side-effects) of a static data member does not occur unless the static data member is itself used in a way that requires the definition of the static data member to exist.

这并没有完全解决 constexpr 的情况,因为(我认为)它讨论的是 odr-used 的静态数据成员的超出 class 的定义(它是与 registrar 更相关),但它很接近。

14.7.1p1:

[...] The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions, default arguments, or exception-specifications of the class member functions, member classes, scoped member enumerations, static data members and member templates [...]

这保证了第二种解决方案有效。请注意,它不保证有关静态数据成员的 in-class 初始值设定项的任何内容。

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:

An explicit instantiation that names a class template specialization is also an explicit instantiation of the same kind (declaration or definition) of each of its members [...]

鉴于这些是显式实例化定义,这似乎足以说服 GCC 为 __reg_ptr 实例化初始化程序。但是,那些显式实例化定义在整个程序中只能出现一次([14.7p5.1]),因此需要格外小心。我认为前两种方案更可靠。