静态存储对象指针作模板非类型参数:MSVC与GCC/Clang行为差异问询
我正在开发一个C个人项目,需要在编译时解析编译时已知、存储在固定大小字符串中的JSON。由于C实现这类编译时操作难度不小,我用到了一些模板技巧,过程中发现了一个跨编译器行为不一致的案例,以下是最小复现代码:
#include <array> #include <cstdint> #include <string_view> static constexpr std::array static_array = {1, 2, 3, 4, 5}; template <const int* test> constexpr int ExtractInt() { return *test; } template <const std::array<int, 5>& SomeStaticArray, std::uint64_t Index> requires(Index < 5) constexpr void Get(std::array<int, 5>& array) { array[Index] = ExtractInt<&(SomeStaticArray[Index])>(); } template <const std::array<int, 5>& SomeStaticArray, std::uint64_t... Indices> requires(sizeof...(Indices) <= 5) constexpr auto GetAllImpl() { std::array<int, 5> array; (Get<SomeStaticArray, Indices>(array), ...); return array; }; template <const std::array<int, 5>& SomeStaticArray> constexpr auto GetAll() { return GetAllImpl<SomeStaticArray, 0, 1, 2, 3, 4>(); } int main() { constexpr auto array = GetAll<static_array>(); static_assert(array[0] == static_array[0]); // Uncomment the next 4 to get gcc and clang to compile // static_assert(array[1] == static_array[1]); // static_assert(array[2] == static_array[2]); // static_assert(array[3] == static_array[3]); // static_assert(array[4] == static_array[4]); // Uncomment the next 4 lines to get msvc to compile // static_assert(array[1] == static_array[0]); // static_assert(array[2] == static_array[0]); // static_assert(array[3] == static_array[0]); // static_assert(array[4] == static_array[0]); return 0; }
这段代码在GCC和Clang中能生成与原静态数组元素完全一致的新数组,但MSVC却认为新数组的所有元素都是原数组的第一个元素。针对这个情况,我有几个疑问:
- 这段代码是否符合C++标准?
- MSVC的参数包展开是否存在问题?(我对此存疑,因为修改Get函数的约束为
Index < 3时,MSVC无法编译) - 将数组引用作为模板非类型参数是否违规?
- 这是否只是MSVC的编译器Bug?
补充说明:在我的实际复杂项目中,把指针改为引用并做相应调整后可以正常编译运行,但上述示例代码中MSVC不接受该修改,而GCC和Clang可以。我需要在编译时创建不基于其他std::string_view的std::string_view(比如处理JSON字符串的Unicode转义),所以用到了这些模板技巧。另外,项目依赖std::expected必须使用/std:c++latest选项,且C20和C23标准下各编译器的行为一致。
问题解答
代码是否符合C++标准
这段代码符合C20及后续标准。C20允许将const std::array<T, N>&作为模板非类型参数,同时&SomeStaticArray[Index]是有效的编译时常量表达式——因为SomeStaticArray是绑定到静态constexpr数组的模板参数,其元素地址在编译时确定,满足指针类型模板非类型参数的要求。MSVC的参数包展开是否有问题
从现象来看,MSVC在处理参数包展开时,错误地将所有Index实例解析为0,导致所有ExtractInt模板都传入了&SomeStaticArray[0]。但修改约束为Index < 3时MSVC无法编译,说明它并非完全不处理参数包,而是在模板参数推导或常量表达式求值阶段出现错误,并非参数包展开本身的基础功能问题。数组引用作为模板非类型参数是否违规
不违规。C++20扩展了模板非类型参数的允许类型,其中包括对固定大小数组的引用(或std::array<T, N>的const引用,因为std::array的大小是编译时已知的模板参数)。只要引用绑定的是具有静态存储期的对象,就符合标准要求。是否为MSVC的编译器Bug
是的,这大概率是MSVC的编译器Bug。GCC和Clang的行为符合预期,而MSVC错误地将不同索引的元素地址统一解析为第一个元素的地址,违反了常量表达式求值规则。结合实际项目中修改为引用后MSVC仍不支持的情况,说明MSVC在处理涉及模板非类型参数的数组元素地址推导时存在实现缺陷。
内容的提问来源于stack exchange,提问作者uxsu

