第
C++中的std::format?如何实现编译期格式检查
C++20的std::format是一个很神奇、很实用的工具,最神奇的地方在于它能在编译期检查字符串的格式是否正确,而且不需要什么特殊的使用方法,只需要像使用普通函数那样传参即可。
#includeformat
inta=1;
std::strings1=std::format(a:{},a);//OK
std::strings2=std::format(a:{},b:{},a);//编译错误
C++20的std::format来自一个著名的开源库{fmt}。在C++20之前,fmt需要为每个字符串字面量创建不同的类型才能实现编译期格式检查。fmt提供了一个FMT_STRING宏以简化使用的流程。
#includefmt/format.h
inta=1;
std::strings1=fmt::format(FMT_STRING(a:{}),a);//OK
std::strings2=fmt::format(FMT_STRING(a:{},b:{}),a);//编译错误
C++20有了consteval后就不用这么别扭了。consteval函数与以前的constexpr函数不同,constexpr函数只有在必须编译期求值的语境下才会在编译期执行函数,而consteval函数在任何情况下都强制编译期求值。std::format就是利用consteval函数在编译期执行代码,来检查字符串参数的格式。
然而std::format自身不能是consteval函数,只好曲线救国,引入一个辅助类型std::format_string,让字符串实参隐式转换为std::format_string。只要这个转换函数是consteval函数,并且把格式检查的逻辑写在这个转换函数里面,照样能实现编译期的格式检查。
这里我们实现了一个极简版的format,可以检查字符串中{}的数量是否与参数的个数相同。format_string的构造函数就是我们需要的隐式转换函数,它是一个consteval函数。若字符串中{}的数量不对,则代码会执行到throw这一行。C++的throw语句不能在编译期求值,因此会引发编译错误,从而实现了在编译期检查出字符串的格式错误。
namespacemy{
templateclass...Args
classformat_string{
private:
std::string_viewstr;
public:
templateclassT
requiresstd::convertible_toconstT,std::string_view
constevalformat_string(constTs)
:str(s)
std::size_tactual_num=0;
for(std::size_ti=0;i+1str.length();i++){
if(str[i]=={str[i+1]==}){
actual_num++;
constexprstd::size_texpected_num=sizeof...(Args);
if(actual_num!=expected_num){
throwstd::format_error(incorrectformatstring);
std::string_viewget()const{returnstr;}
templateclass...Args
std::stringformat(format_stringstd::type_identity_tArgs...fmt,Args...args){
//省略具体的格式化逻辑
}
有一个细节,此处format函数的参数写的是format_stringstd::type_identity_tArgs...,直接写format_stringArgs...是无法隐式转换的,因为模板实参推导(templateargumentdeduction)不会考虑隐式转换,C++20提供了一个工具std::type_identity可以解决这个问题。std::type_identity其实就是一个关于类型的恒等函数,但是这么倒腾一下就能在模板实参推导中建立非推导