C++ constexpr变量通过static_assert仍报非常量表达式错误
编译期字符串拼接常量表达式错误分析
问题现象
实现编译期字符串拼接功能时,注释代码末尾std::cout << joined<a,b,c> << std::endl;行可正常编译,取消注释则触发编译错误,复现代码如下:
#include <iostream> constexpr const char a[] = "hello "; constexpr const char b[] = "dear "; constexpr const char c[] = "world!"; constexpr size_t size(const char* s) { int i = 0; while(*s!=0) { ++i; ++s; } return i; } template <typename... Is, typename = std::enable_if_t<(... && std::is_same_v<const char*, Is>)>> constexpr size_t calc_size(Is... values) { return (0 + ... + size(values)); } constexpr bool strings_equal(char const * a, char const * b) { return *a == *b && (*a == '\0' || strings_equal(a + 1, b + 1)); } template <const char*... S> class cxpr_string { public: constexpr cxpr_string() : buf_{}, size_{0} { int i=0; ( [&]() { const size_t max = size(S); for (int i=0; i < max; ++i) { buf_[size_++] = S[i]; } }(), ...); buf_[size_++] = 0; } constexpr const char* get() const { return buf_; } private: char buf_[calc_size(S...)+1] = { 0 }; size_t size_; }; template <const char*... ptr> constexpr auto joined = cxpr_string<ptr...>().get(); int main() { static_assert(strings_equal(cxpr_string<a,b,c>().get(), "hello dear world!")); std::cout << joined<a,b,c> << std::endl; // 取消注释即报错 std::cout << cxpr_string<a, b, c>().get() << std::endl; }
报错信息如下:
<source>: In instantiation of 'constexpr const char* const joined<(& a), (& b), (& c)>': <source>:56:18: required from here <source>:50:16: error: '(const char*)(&<anonymous>.cxpr_string<(& a), (& b), (& c)>::buf_)' is not a constant expression 50 | constexpr auto joined = cxpr_string<ptr...>().get(); | ^~~~~~
不同编译器表现存在差异:GCC 12.1、Clang 14.0.0环境下编译失败,MSVC 19.31环境下可编译但运行失败。
根因说明
问题本质是违反了C++标准对常量表达式的生命周期约束:
static_assert中的调用可正常通过校验:该场景下临时创建的cxpr_string<a,b,c>对象全程在常量求值上下文内,strings_equal直接逐字符读取临时对象内部buf_的内容完成比较,不需要将buf_的地址持久存储到跨表达式的常量中,因此符合常量表达式规则。joined变量初始化非法:joined是命名空间作用域的constexpr变量,初始化式返回的是临时cxpr_string对象内部buf_数组的首地址。C++标准明确禁止将临时对象的地址作为常量表达式的最终结果——临时对象在所属完整表达式结束后就会被销毁,将其地址存储到全局常量中必然产生悬空指针,因此编译器判定该表达式不属于合法常量表达式。- 直接输出临时对象
get()结果可正常编译:std::cout << cxpr_string<a, b, c>().get() << std::endl;中临时对象的生命周期覆盖整个cout表达式的执行周期,编译器不需要将buf_地址持久存储到全局常量中,因此不会触发规则限制。 - MSVC的异常表现属于标准符合性缺陷:它错误接受了非法的常量表达式初始化,但运行时临时对象已经销毁,访问悬空指针直接导致运行崩溃。
修复方案
不要将临时对象内部缓冲区的裸指针存储为全局常量,改为直接存储cxpr_string类型的constexpr实例:
template <const char*... ptr> constexpr cxpr_string<ptr...> joined{};
使用时调用joined<a,b,c>.get()即可。此时joined本身是静态存储期的常量对象,内部buf_的地址固定合法,不存在悬空指针问题。
内容的提问来源于stack exchange,提问作者glades
相关产品推荐
相关产品推荐

