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
的每个专门化的默认构造函数的定义都是实例化的,以处理main
:中a
、b
和c
的默认初始化
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]),因此需要格外小心。我认为前两种解决方案更可靠。
- 多成员Constexpr结构初始化
- constexpr构造函数需要常量成员函数时出现问题
- 添加静态constexpr成员是否会更改结构/类的内存映射
- 静态 constexpr 类成员变量对多线程读取是否安全?
- C++:初始化 constexpr 构造函数中的成员数组
- 初始化模板化类中的静态 constexpr 成员
- 静态 constexpr 成员变量初始化
- 静态数据成员:它"const declaration / constexpr definition"起作用?
- 使用静态 constexpr 成员的未解析外部符号
- 使用模板参数还包括 constexpr 成员函数enable_if单独定义和声明模板成员函数
- Constexpr成员函数
- 在模板定义中调用非静态constexpr成员函数
- 类成员参数中的Constexpr
- 在构造函数中使用 constexpr 成员
- 不带初始值设定项的 constexpr 静态数据成员
- emplace_back会导致静态 constexpr 成员上出现链接错误
- l值引用对象上的Constexpr成员函数:Clang和gcc不同意
- 在类外部初始化的 constexpr 静态成员的声明中是否需要 constexpr 说明符
- 现在允许重新定义 constexpr 静态数据成员吗?(但不是内联常量)?
- 是否可以以编程方式初始化 constexpr std::array 成员