为何std::vector::at在C++26 constexpr范围for循环中失效?
场景与代码
在GCC 14.2环境下尝试实现基于C++26(GCC可选特性)的编译期解析器,编写了如下简化代码:
#include <iostream> #include <ranges> #include <string_view> #include <vector> using namespace std::string_view_literals; template <class To> inline constexpr auto static_caster = []<class From>(From &&from) -> To requires requires { static_cast<To>(std::declval<From>()); } { return static_cast<To>(std::forward<From>(from)); }; struct Test { bool value; constexpr Test(const std::string_view input) { const std::vector<std::string_view> lines = input | std::views::split('\n') | std::views::transform(static_caster<std::string_view>) | std::ranges::to<std::vector>(); // const std::string_view line = lines.at(0); for (const std::string_view line : lines) { const std::vector<std::string_view> line_tokens = line | std::views::split(' ') | std::views::transform(static_caster<std::string_view>) | std::ranges::to<std::vector>(); const std::string_view kind = line_tokens.at(0); this->value = "v" == kind; } } }; int main(int argc, char *argv[]) { constexpr bool value = Test("v 1.0 2.0 3.0\n"sv).value; std::cout << value; return 0; }
编译错误信息
编译时触发如下错误:
In file included from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/vector:68, from <source>:4: /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/stl_vector.h: In function 'int main(int, char**)': <source>:33:52: in 'constexpr' expansion of 'Test(std::literals::string_view_literals::operator""sv(((const char*)"v 1.0 2.0 3.0\012"), 14))' <source>:26:57: in 'constexpr' expansion of 'line_tokens.std::vector<std::basic_string_view<char> >::at(0)' /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/stl_vector.h:1333:16: in 'constexpr' expansion of '((const std::vector<std::basic_string_view<char> >*)this)->std::vector<std::basic_string_view<char> >::_M_range_check(__n)' /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/stl_vector.h:1292:35: error: call to non-'constexpr' function 'void std::__throw_out_of_range_fmt(const char*, ...)' 1292 | __throw_out_of_range_fmt(__N("vector::_M_range_check: __n " | ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1293 | "(which is %zu) >= this->size() " | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1294 | "(which is %zu)"), | ~~~~~~~~~~~~~~~~~~ 1295 | __n, this->size()); | ~~~~~~~~~~~~~~~~~~ In file included from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/new_allocator.h:35, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/x86_64-linux-gnu/bits/c++allocator.h:33, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/allocator.h:46, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/string:45, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/locale_classes.h:42, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/ios_base.h:43, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/ios:46, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/ostream:42, from /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/iostream:43, from <source>:1: /opt/compiler-explorer/gcc-trunk-20241219/include/c++/15.0.0/bits/functexcept.h:82:3: note: 'void std::__throw_out_of_range_fmt(const char*, ...)' declared here 82 | __throw_out_of_range_fmt(const char*, ...) __attribute__((__noreturn__,__cold__)) | ^~~~~~~~~~~~~~~~~~~~~~~~
已知两个现象:
- 直接用
lines.at(0)获取固定行而非遍历lines时,代码正常编译; - 移除输入常量中的
\n也能正常编译。
问题原因
核心问题是输入字符串末尾的\n导致std::views::split生成了一个空的子范围:
当输入是"v 1.0 2.0 3.0\n"时,split('\n')会把字符串拆分成两个部分:第一部分是有效内容"v 1.0 2.0 3.0",第二部分是末尾\n之后的空字符串。
在编译期上下文中,constexpr代码的所有分支路径都必须是合法的——即使运行时不会走到错误分支,编译器也会检查所有可能的执行路径。当遍历lines时,第二个空字符串会进入循环:对空字符串执行split(' ')得到的是空的token列表,此时调用line_tokens.at(0)会触发越界检查,而GCC的std::vector::at在越界时调用的std::__throw_out_of_range_fmt并非constexpr函数,因此编译报错。
而直接取lines.at(0)时,编译器只会验证这一个路径的合法性(第一个元素是有效行,token列表非空);移除\n则不会生成空行,自然不会触发错误路径。
解决方法
方法1:过滤空行
在生成lines时,添加过滤空字符串的视图,跳过空行:
const std::vector<std::string_view> lines = input | std::views::split('\n') | std::views::transform(static_caster<std::string_view>) | std::views::filter([](std::string_view sv) { return !sv.empty(); }) | // 新增过滤逻辑 std::ranges::to<std::vector>();
方法2:避免使用at(),改用operator[]配合前置检查
如果能确保token列表非空(比如过滤后),可以用operator[]代替at()——operator[]不做越界检查,也就不会调用抛出异常的非constexpr函数。或者在调用at()前先判断line_tokens的大小:
if (!line_tokens.empty()) { const std::string_view kind = line_tokens.at(0); this->value = "v" == kind; }
方法3:处理末尾的分隔符
可以提前移除输入字符串末尾的\n,或者使用std::views::split的替代方式避免生成空范围。
内容的提问来源于stack exchange,提问作者Torsten Knodt

