如何在编译期验证字符串?C++模板元编程实现问询
我尝试让以下C++代码正常运行:
#include <string> #include <tuple> #include <print> template<char Specifier> struct ParseSpecifier; template<> struct ParseSpecifier<'s'> { static void parse() { std::println("string"); } }; template<> struct ParseSpecifier<'i'> { static void parse() { std::println("integer"); } }; template<std::size_t Index> void parse_format(const std::string_view format) { if (Index == format.size()) return; ParseSpecifier<format[Index]>::parse(); parse_format<Index + 1>(format); } int main() { parse_format<0>("iiisss"); }
我知道fmt这类库能实现编译期验证格式字符串,所以这肯定能做到。我的最终目标不是打印"string"和"integer",而是根据给定字符串中的'i'和's'生成std::tuple,这个字符串始终是编译期已知的。我刚学模板元编程,从小功能开始练手。我知道函数参数不能作为constexpr,std::string_view也没法当模板参数;如果用template<char... Chars>,调用起来又太麻烦。请问能不能修改现有代码让它运行?如果不行,编译期验证字符串的最佳策略是什么?另外,最终的函数会像fmt::format那样,接收一个编译期已知参数和运行期参数。
一、修改现有代码使其运行的方法
原代码核心问题在于:format[Index]是运行期值,无法作为模板参数(模板参数必须是编译期常量)。要解决这个问题,得把格式字符串的信息提升到编译期模板参数层面,同时兼顾调用简洁性,C++20及以后有两种实用方案:
方法1:用C++20 constexpr字符串模板参数
C++20允许将const char*作为非类型模板参数,只要它是编译期常量:
#include <string> #include <tuple> #include <print> template<char Specifier> struct ParseSpecifier; template<> struct ParseSpecifier<'s'> { static void parse() { std::println("string"); } }; template<> struct ParseSpecifier<'i'> { static void parse() { std::println("integer"); } }; // 模板参数接收编译期常量字符串 template<const char* Format, std::size_t Index = 0> void parse_format() { // 用if constexpr确保编译期终止递归 if constexpr (Index == std::char_traits<char>::length(Format)) return; ParseSpecifier<Format[Index]>::parse(); parse_format<Format, Index + 1>(); } // 定义编译期常量字符串 constexpr auto fmt_str = "iiisss"; int main() { parse_format<fmt_str>(); }
方法2:用用户定义字面量简化调用
如果想让调用更贴近fmt::format的风格,可以用用户定义字面量把字符串转成编译期字符序列类型:
#include <string> #include <tuple> #include <print> template<char... Chars> struct FormatString { static constexpr const char value[] = {Chars..., '\0'}; }; // 字面量运算符:把字符串转成编译期字符序列结构体 template<char... Chars> constexpr auto operator""_fmt() { return FormatString<Chars...>{}; } template<char Specifier> struct ParseSpecifier; template<> struct ParseSpecifier<'s'> { static void parse() { std::println("string"); } }; template<> struct ParseSpecifier<'i'> { static void parse() { std::println("integer"); } }; template<typename Format, std::size_t Index = 0> void parse_format() { if constexpr (Index == sizeof(Format::value) - 1) return; ParseSpecifier<Format::value[Index]>::parse(); parse_format<Format, Index + 1>(); } int main() { parse_format<decltype("iiisss"_fmt)>(); }
二、编译期验证+生成tuple的最佳策略
你的最终目标是生成对应类型的std::tuple,推荐结合编译期字符序列、模板递归和C++20的concepts来实现,既保证类型安全,又兼顾调用简洁:
1. 字符到类型的映射
先定义格式字符对应的数据类型:
template<char C> struct CharToType; template<> struct CharToType<'i'> { using type = int; }; template<> struct CharToType<'s'> { using type = std::string; }; template<char C> using CharToType_t = typename CharToType<C>::type;
2. 从字符序列生成tuple类型
通过模板递归展开字符序列,拼接成目标tuple:
template<char... Chars> struct FormatToTuple; // 递归展开字符序列 template<char First, char... Rest> struct FormatToTuple<First, Rest...> { using type = std::tuple<CharToType_t<First>, typename FormatToTuple<Rest...>::type>; }; // 递归终止条件:空字符序列对应空tuple template<> struct FormatToTuple<> { using type = std::tuple<>; }; template<char... Chars> using FormatToTuple_t = typename FormatToTuple<Chars...>::type;
3. 编译期验证格式合法性
用concept限制只能传入合法的格式字符:
template<char C> concept ValidSpecifier = (C == 'i' || C == 's'); template<char... Chars> concept ValidFormatString = (ValidSpecifier<Chars> && ...);
4. 封装成类似fmt的调用接口
结合用户定义字面量,让调用更直观:
template<char... Chars> requires ValidFormatString<Chars...> constexpr auto make_format_tuple() { return FormatToTuple_t<Chars...>{}; } template<char... Chars> constexpr auto operator""_fmt() { return []{ return make_format_tuple<Chars...>(); }; } int main() { // 编译期生成std::tuple<int, int, int, std::string, std::string, std::string> auto t = "iiisss"_fmt(); std::println("tuple size: {}", std::tuple_size_v<decltype(t)>); // 输出6 }
原代码无法直接修改的原因
原代码中parse_format的format是函数参数,属于运行期值——哪怕你传入的是字符串字面量,在函数内部它还是std::string_view(运行期类型),无法提取编译期的字符值作为模板参数。必须把字符串的字符信息提升到编译期的模板参数层面,才能让ParseSpecifier的模板参数是编译期常量。
内容的提问来源于stack exchange,提问作者Eshy

