基于非模板参数实例化模板:GCC可运行CLANG报错,是否符合C++标准?
当使用成员或非类型参数实例化模板时,Clang会报错:error: non-type template argument refers to subobject
我想通过带有静态数组成员的模板结构体让编译器分配内存,要求编译器为每个实例化的结构体静态分配数组内存。同时我希望用constexpr数组配置这些内存池,再在编译期将其整合到另一个数组中。
这段代码在GCC 11.3及主干版本中可以正常运行,但在Clang里无法通过编译。如果这是Clang的问题,有没有解决办法?
struct NamedPoolConfig { const std::string_view name; const unsigned int pool_size; }; inline constexpr std::array pool_config = std::to_array<const NamedPoolConfig>({ { .name = "A", .pool_size = 512, }, { .name = "B", .pool_size = 1024, }, { .name = "C", .pool_size = 256, }, }); template<std::string_view const& PoolName, std::size_t SIZE> struct StaticPool { inline static std::array<std::uint8_t, SIZE> static_arr; static constexpr std::span<std::uint8_t> pool = std::span(static_arr.data(), static_arr.size()); }; struct NamedPool { std::string_view name; std::span<std::uint8_t> pool; }; template<const decltype(pool_config)& arr> consteval auto make_pools() { return []<std::size_t... Is>(std::index_sequence<Is...>) { return std::array{NamedPool{ .name = std::string_view(arr[Is].name), .pool = StaticPool<arr[Is].name, arr[Is].pool_size>::pool, }...}; }(std::make_index_sequence<arr.size()>{}); } inline auto pool_array = make_pools<pool_config>();
我有点怀疑自己的代码是在“取巧”——因为编译器似乎不是基于我指定的名称,而是基于变量名称来实例化这些静态池,这可能会在未来多池数组的场景下出问题,但这是另一个问题。
标准合规性分析
你的代码不符合C++标准,Clang的报错是正确的,GCC的支持属于非标准扩展。
根据C++标准,当引用类型作为非类型模板参数时,它所绑定的对象必须满足以下条件之一:
- 是具有外部或内部链接的对象;
- 是模板参数对象;
- 是临时对象(仅限特定场景)。
而arr[Is].name是pool_config数组元素的成员,属于子对象,这类对象没有独立的链接属性,因此不能作为引用类型的非类型模板参数传递。GCC在这里放宽了标准限制,允许这种用法,但这并不符合C++标准的要求。
解决方法
方法1:使用独立的有链接属性的名称对象
把每个name定义为独立的、具有内部链接的constexpr std::string_view对象,这样就能合法地作为模板参数传递:
// 定义独立的、具有内部链接的constexpr名称对象 inline constexpr std::string_view pool_name_A = "A"; inline constexpr std::string_view pool_name_B = "B"; inline constexpr std::string_view pool_name_C = "C"; struct NamedPoolConfig { const std::string_view& name; // 改为引用类型,指向上面的独立对象 const unsigned int pool_size; }; inline constexpr std::array pool_config = std::to_array<const NamedPoolConfig>({ { .name = pool_name_A, .pool_size = 512, }, { .name = pool_name_B, .pool_size = 1024, }, { .name = pool_name_C, .pool_size = 256, }, }); template<std::string_view const& PoolName, std::size_t SIZE> struct StaticPool { inline static std::array<std::uint8_t, SIZE> static_arr; static constexpr std::span<std::uint8_t> pool = std::span(static_arr.data(), static_arr.size()); }; struct NamedPool { std::string_view name; std::span<std::uint8_t> pool; }; template<const decltype(pool_config)& arr> consteval auto make_pools() { return []<std::size_t... Is>(std::index_sequence<Is...>) { return std::array{NamedPool{ .name = arr[Is].name, .pool = StaticPool<arr[Is].name, arr[Is].pool_size>::pool, }...}; }(std::make_index_sequence<arr.size()>{}); } inline auto pool_array = make_pools<pool_config>();
方法2:C++20+下使用值传递的std::string_view模板参数
将StaticPool的模板参数改为值传递的std::string_view(而非引用),这样字符串字面量或constexpr std::string_view对象都可以直接作为参数,无需依赖对象的链接属性:
struct NamedPoolConfig { const std::string_view name; const unsigned int pool_size; }; inline constexpr std::array pool_config = std::to_array<const NamedPoolConfig>({ { .name = "A", .pool_size = 512, }, { .name = "B", .pool_size = 1024, }, { .name = "C", .pool_size = 256, }, }); // 修改模板参数为值传递的std::string_view template<std::string_view PoolName, std::size_t SIZE> struct StaticPool { inline static std::array<std::uint8_t, SIZE> static_arr; static constexpr std::span<std::uint8_t> pool = std::span(static_arr.data(), static_arr.size()); }; struct NamedPool { std::string_view name; std::span<std::uint8_t> pool; }; template<const decltype(pool_config)& arr> consteval auto make_pools() { return []<std::size_t... Is>(std::index_sequence<Is...>) { return std::array{NamedPool{ .name = arr[Is].name, .pool = StaticPool<arr[Is].name, arr[Is].pool_size>::pool, }...}; }(std::make_index_sequence<arr.size()>{}); } inline auto pool_array = make_pools<pool_config>();
这种方式更简洁,且完全符合C++20标准,同时能解决你的问题。
内容的提问来源于stack exchange,提问作者JD.

