如何为std::span实现remove_prefix并通用化parse函数?
背景需求
原本希望使用std::basic_string_view<Token>作为解析器输入,但由于Token并非平凡类,无法满足std::basic_string_view对元素类型的要求,因此改用std::span<Token>。需要实现类似std::basic_string_view::remove_prefix的功能,用于逐个匹配并消耗Token元素。
派生std::span的编译错误及修复
尝试从std::span派生自定义Span类并添加remove_prefix成员函数时,出现编译错误:
[ 50.0%] g++.exe -Wall -std=c++20 -fexceptions -g -c F:\code\test_crtp_twoargs\main.cpp -o obj\Debug\main.o F:\code\test_crtp_twoargs\main.cpp: In member function 'constexpr void Span<T>::remove_prefix(std::size_t)': F:\code\test_crtp_twoargs\main.cpp:52:17: error: there are no arguments to 'subspan' that depend on a template parameter, so a declaration of 'subspan' must be available [-fpermissive] 52 | *this = subspan(n); | ^~~~~~~ F:\code\test_crtp_twoargs\main.cpp:52:17: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated) F:\code\test_crtp_twoargs\main.cpp: In instantiation of 'constexpr void Span<T>::remove_prefix(std::size_t) [with T = Token; std::size_t = long long unsigned int]': F:\code\test_crtp_twoargs\main.cpp:113:29: required from 'constexpr bool char_<T>::visit(v&) const & [with T = Token; v = Span<Token>]' F:\code\test_crtp_twoargs\main.cpp:125:24: required from 'constexpr bool parse(Span<T>&, const Parser&) [with Parser = char_<Token>; T = Token]' F:\code\test_crtp_twoargs\main.cpp:141:10: required from here F:\code\test_crtp_twoargs\main.cpp:52:24: error: 'subspan' was not declared in this scope, and no declarations were found by argument-dependent lookup at the point of instantiation [-fpermissive] 52 | *this = subspan(n); | ~~~~~~~^~~ F:\code\test_crtp_twoargs\main.cpp:52:24: note: declarations in dependent base 'std::span<Token, 18446744073709551615>' are not found by unqualified lookup F:\code\test_crtp_twoargs\main.cpp:52:24: note: use 'this->subspan' instead F:\code\test_crtp_twoargs\main.cpp:52:15: error: no match for 'operator=' (operand types are 'Span<Token>' and 'std::span<Token, 18446744073709551615>') 52 | *this = subspan(n); | ~~~~~~^~~~~~~~~~~~ F:\code\test_crtp_twoargs\main.cpp:44:7: note: candidate: 'constexpr Span<Token>& Span<Token>::operator=(const Span<Token>&)' 44 | class Span : public std::span<T> | ^~~~ F:\code\test_crtp_twoargs\main.cpp:44:7: note: no known conversion for argument 1 from 'std::span<Token, 18446744073709551615>' to 'const Span<Token>&' F:\code\test_crtp_twoargs\main.cpp:44:7: note: candidate: 'constexpr Span<Token>& Span<Token>::operator=(Span<Token>&&)' F:\code\test_crtp_twoargs\main.cpp:44:7: note: no known conversion for argument 1 from 'std::span<Token, 18446744073709551615>' to 'Span<Token>&&'
根据提示修改remove_prefix函数后,代码可正常编译,修复后的代码如下:
constexpr void remove_prefix(std::size_t n) { auto& self = static_cast<std::span<T>&>(*this); self = self.subspan(n); }
通用parse函数的问题与解决方法
希望让parse函数同时接受std::string_view和自定义Span<Token>类型的输入,修改后的parse函数如下:
template <typename Parser, typename T> constexpr bool parse(ViewerT<T> &input, Parser const& parser) noexcept { return parser.visit(input); }
但出现模板参数推导失败的编译错误:
[ 50.0%] g++.exe -Wall -std=c++20 -fexceptions -g -c F:\code\test_crtp_twoargs\main.cpp -o obj\Debug\main.o F:\code\test_crtp_twoargs\main.cpp: In function 'int main()': F:\code\test_crtp_twoargs\main.cpp:143:24: error: no matching function for call to 'parse(Span<Token>&, char_<Token>&)' 143 | bool result = parse(token_stream_view, p); | ~~~~~^~~~~~~~~~~~~~~~~~~~~~ F:\code\test_crtp_twoargs\main.cpp:125:16: note: candidate: 'template<class Parser, class T> constexpr bool parse(ViewerT<T>&, const Parser&)' 125 | constexpr bool parse(ViewerT<T> &input, Parser const& parser) noexcept { | ^~~~~ F:\code\test_crtp_twoargs\main.cpp:125:16: note: template argument deduction/substitution failed: F:\code\test_crtp_twoargs\main.cpp:143:24: note: couldn't deduce template parameter 'T' 143 | bool result = parse(token_stream_view, p); | ~~~~~^~~~~~~~~~~~~~~~~~~~~~
解决方法
方案1:使用C++20概念(Concept)约束输入类型
定义一个Viewer概念,约束所有符合解析器输入要求的类型(包括std::string_view和自定义Span):
#include <concepts> template<typename V> concept Viewer = requires(V v) { // 要求类型有value_type嵌套类型 typename V::value_type; // 要求支持remove_prefix方法 { v.remove_prefix(std::size_t{}) } -> std::same_as<void>; // 可根据解析器需求添加其他必要接口,比如empty()、front()等 { v.empty() } -> std::same_as<bool>; { v.front() } -> std::convertible_to<const typename V::value_type&>; };
修改parse函数,直接使用Viewer概念作为输入类型约束:
template <typename Parser, Viewer V> constexpr bool parse(V &input, const Parser& parser) noexcept { return parser.visit(input); }
这样编译器可以自动推导模板参数,无需显式指定类型。
方案2:取消绑定T的模板别名,直接接受任意输入类型
如果不需要提前绑定T,可以直接将parse函数改为接受任意类型V,只要该类型能被Parser::visit处理:
template <typename Parser, typename V> constexpr bool parse(V &input, const Parser& parser) noexcept { return parser.visit(input); }
这种方式最简洁,只要Parser的visit方法能处理V类型(比如std::string_view和Span<Token>都实现了对应的接口),就能自动推导模板参数。
方案3:为输入类型提供统一的类型萃取
如果需要明确获取输入类型的value_type,可以定义一个类型萃取模板:
// 通用萃取模板 template<typename V> struct viewer_value_type { using type = typename V::value_type; }; // 为std::basic_string_view特化(可选,因为它本身已有value_type) template<typename CharT> struct viewer_value_type<std::basic_string_view<CharT>> { using type = CharT; }; // 别名模板简化使用 template<typename V> using viewer_value_type_t = typename viewer_value_type<V>::type;
修改parse函数,使用萃取的类型:
template <typename Parser, typename V> constexpr bool parse(V &input, const Parser& parser) noexcept { // 若需要使用T,可通过viewer_value_type_t<V>获取 using T = viewer_value_type_t<V>; return parser.visit(input); }
此方案同样支持自动推导模板参数,无需显式指定。
内容的提问来源于stack exchange,提问作者ollydbg

