亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

C++ 的 format 和 vformat 函數(shù)示例詳解

 更新時(shí)間:2025年02月10日 09:36:53   作者:王曉華-吹泡泡的小貓  
傳統(tǒng)C庫(kù)的printf系列函數(shù)存在安全問(wèn)題,而C++推薦的基于流格式化輸入輸出雖然解決了安全性問(wèn)題,但在易用性方面仍顯不足,C++11引入了新的C風(fēng)格字符串格式化函數(shù),但類(lèi)型安全問(wèn)題依舊存在,下面通過(guò)本文介紹C++ 的 format 和 vformat 函數(shù)示例,感興趣的朋友一起看看吧

1 C++ 字符串格式化的困境

1.1 “冗長(zhǎng)的裹腳布”

? 傳統(tǒng) C 庫(kù)的 printf() 系列函數(shù)的優(yōu)點(diǎn)就是函數(shù)調(diào)用更自然,并且格式化信息與參數(shù)分離,代碼結(jié)構(gòu)清晰,但是因?yàn)榘踩詥?wèn)題一直被詬病。C++ 更推薦基于流的格式化輸入輸出代替?zhèn)鹘y(tǒng) C 庫(kù)的 printf() 系列函數(shù),雖然解決了 printf() 系列函數(shù)的安全性問(wèn)題,但是在易用性方面也是有點(diǎn)反人類(lèi)。先不說(shuō)輸入和輸出,單就說(shuō)字符串的格式化,對(duì) C++ 的使用者來(lái)說(shuō)簡(jiǎn)直是“一把辛酸淚”。舉個(gè)簡(jiǎn)單的例子,把浮點(diǎn)數(shù)格式化字符串,小數(shù)點(diǎn)后保留 3 位小數(shù),用 sprintf() 實(shí)現(xiàn)非常簡(jiǎn)單:

char buf[64];
sprintf(buf, "%.3f", 1.0/3.0);  //buf 內(nèi)是 0.333

如果用 C++,就是這樣一番風(fēng)格:

std::stringstream ss;
ss << std::fixed << std::setw(5) << std::setprecision(3) << 1.0/3.0;
std::string str = ss.str();  // str 是 0.333

? 不得不說(shuō),C++ 的這個(gè)風(fēng)格真的很“學(xué)院派”,各種控制 IO 流的操作符如果用來(lái)出題考試,定能讓學(xué)渣們生不如死。這個(gè)例子只是格式化一個(gè)浮點(diǎn)數(shù),如果要將多個(gè)不同類(lèi)型的數(shù)據(jù)格式化到一個(gè)字符串中,需要多少個(gè)控制符拼接?像裹腳布,冗長(zhǎng)且不直觀。大多數(shù) C++ 程序員對(duì)于格式化字符串不得不繼續(xù)用 sprintf(),但是 sprintf() 除了安全性問(wèn)題,還存在類(lèi)型支持不足的問(wèn)題。它只支持幾種內(nèi)置類(lèi)型,不支持標(biāo)準(zhǔn)庫(kù)中的各種容器,更不用說(shuō)用戶(hù)自定義的類(lèi)型了。

1.2 C++ 11 的小革新

? C++ 11 提供了一個(gè)新的 C 風(fēng)格字符串格式化函數(shù):

int snprintf(char* buffer, std::size_t buf_size, const char* format, ...);

除了 buf_size 參數(shù)有助于防止 buffer 溢出的好處之外,這個(gè)函數(shù)還可以計(jì)算對(duì)指定的參數(shù)進(jìn)行文本格式化后需要的存儲(chǔ)空間:

const char *fmt = "sqrt(2) = %f";
int sz = std::snprintf(nullptr, 0, fmt, std::sqrt(2));
std::vector<char> buf(sz + 1); // note +1 for null terminator
std::snprintf(&buf[0], buf.size(), fmt, std::sqrt(2));

借助于 C++ 11 的函數(shù)參數(shù)包(關(guān)于參數(shù)包可參考《C++ 的 “…”與可變參數(shù)列表》一篇),可以實(shí)現(xiàn)一個(gè)具有 C++ 風(fēng)格的文本格式化函數(shù)。在 stackoverflow 網(wǎng)站,有人給出了這樣一個(gè)解決方案:

template<typename ... Args>
std::string string_format(const std::string& format, Args ... args) {
    int size_s = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; // Extra space for '\0'
    if (size_s <= 0) { 
        throw std::runtime_error("Error during formatting."); 
    }
    auto size = static_cast<size_t>(size_s);
    std::unique_ptr<char[]> buf(new char[size]);
    std::snprintf(buf.get(), size, format.c_str(), args ...);
    return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside
}
//using string_format()
std::string s = string_format("%s is %d years old.", "Emma", 5);

雖然曲折,但是在你的編譯器升級(jí)到 C++ 20 之前,還可一用。不過(guò)需要注意,雖然這個(gè)函數(shù)解決了溢出問(wèn)題,但是類(lèi)型安全問(wèn)題仍然存在。

2 C++ 20 的 format 函數(shù)

2.1 format 函數(shù)

? 雖然 boost 中的 format 庫(kù)存在很長(zhǎng)時(shí)間了,但是不知道是這個(gè)庫(kù)的效率問(wèn)題還是其他原因,一直沒(méi)有入 C++ 標(biāo)準(zhǔn)委員會(huì)的法眼。很多 C++ 程序員對(duì) Python 的 format() 函數(shù)“垂涎三尺”,fmtlib 庫(kù)的出現(xiàn)終于緩解了這種渴望。更好的消息是 fmtlib 的一部分已經(jīng)進(jìn)入了 C++ 20 標(biāo)準(zhǔn),比如 format() 函數(shù)和 vformat() 函數(shù)。來(lái)看幾個(gè) format() 函數(shù)的使用例子:

std::string name("Bob");
auto result = std::format("Hello {}!", name);  // Hello Bob!
//03:15:30
result = std::format("Full Time Format: {:%H:%M:%S}\n", 3h + 15min + 30s);
//***3.14159
std::print("{:*>10.5f}", std::numbers::pi);

是不是很像 Python?

2.2 fmt 標(biāo)準(zhǔn)格式

? fmt 參數(shù)表示字符串格式化的格式,類(lèi)型是 std::format_string(提案中最初版本是 std::string_view,后來(lái)是 std::format_string,與 C++ 23 補(bǔ)充的 std::print() 函數(shù)一致)。顯然,這個(gè)“格式化字符串”是有格式的,一般來(lái)說(shuō),除了 “{” 和 “}” 兩個(gè)特殊字符外,其他的字符都會(huì)原樣復(fù)制到輸出結(jié)果中。“{” 和 “}” 是表示格式的特殊符號(hào),如果確實(shí)需要輸出 “{” 和 “}”,就需要用轉(zhuǎn)義序列 “{{” 和 “}}”代替。實(shí)際上一對(duì) “{” 和 “}” 符號(hào)組成了一個(gè)占位符,這個(gè)占位符的語(yǔ)法描述是:

其中 arg-id 和 format-spec 都是可選的,也就是說(shuō),一對(duì)空的大括號(hào) “{}” 也是合法的格式字符串:

auto result = std::format("{} is {} years old.", "Kitty", 5);  //Kitty is 5 years old.

在這種情況下,每一對(duì)大括號(hào)與 args 表示的參數(shù)列表中的參數(shù)按順序一一對(duì)應(yīng)。如果 args 參數(shù)列表中的參數(shù)個(gè)數(shù)比 fmt 中的格式化占位符多,則不匹配的參數(shù)會(huì)被忽略,但不會(huì)報(bào)錯(cuò)。反過(guò)來(lái),如果 args 參數(shù)列表中的參數(shù)個(gè)數(shù)比 fmt 中的格式化占位符少,則需要注意編譯器的行為。資料 [9] 的 P2216 提案已經(jīng)成為 C++ 23 的內(nèi)容,所以 C++ 23 版本的編譯器,會(huì)報(bào)編譯錯(cuò)誤。對(duì)于 C++ 20 版本的編譯器,則要看它對(duì)這個(gè)提案的支持情況。還不支持 P2216 的編譯器不會(huì)報(bào)編譯錯(cuò)誤,但是代碼會(huì)在運(yùn)行時(shí)拋出 format_error 異常。

auto result = std::format("{} is {} years old.", "Kitty", 5, 43.67);  //運(yùn)行正常,43.67 被忽略
auto result = std::format("{} is {} years old.", "Kitty");  //取決于編譯器

2.2.1 實(shí)參(占位符)索引(arg-id)

? 如果格式化字符串中需要強(qiáng)調(diào)占位符與參數(shù)的位置關(guān)系,則需要指定 arg-id 參數(shù)。arg-id 用于指定占位符代表的格式化值在參數(shù)列表 args 中的下標(biāo)。比如:

std::string dogs{ "dogs" };
std::string emma{ "Emma" };
auto result = std::format("{0} loves {1}, but {1} don't love {0}.", emma, dogs);
//Emma loves dogs, but dogs don't love Emma.

需要注意,實(shí)參索引要么都不使用,要么就全部指定,格式化字符串不支持部分使用索引的情況,比如這樣的代碼是錯(cuò)誤的:

auto s = std::format("{1} is a good {}, but Dos is {0} !\n", 6.22, apple); //error

2.2.2 格式說(shuō)明(format-spec)

? 格式說(shuō)明位于冒號(hào)的右邊,它的語(yǔ)法形式是:

fill-and-align(optional) sign(optional) #(optional) 0(optional) width(optional) precision(optional) L(optional) type(optional) 

看起來(lái)稍顯復(fù)雜,但是無(wú)非就是填充、對(duì)齊、符號(hào)、域?qū)?、精度等?nèi)容。首先看看填充和對(duì)齊(fill-and-align),填充字符可以是除了 “{” 和 “}” 之外的任意字符,緊跟在后面的就是對(duì)齊標(biāo)志。對(duì)齊標(biāo)志也是一個(gè)字符,用 “<” 表示強(qiáng)制左對(duì)齊,用 “>” 表示強(qiáng)制右對(duì)齊,對(duì)齊標(biāo)志有三種,用 “^” 表示居中對(duì)齊,會(huì)在值的前面插入 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n?⌋ 個(gè)填充字符,在值的后面插入 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n?⌉ 個(gè)填充字符(注意取整方向)。

auto str = std::format("{:*<7}", 42); // str 的值為 "42*****"
auto str = std::format("{:*>7}", 42); // str 的值為 "*****42"
auto str = std::format("{:*^7}", 42); // str 的值為 "**42***"

如果不指定填充和對(duì)齊控制字符,則用默認(rèn)的填充和對(duì)齊控制。默認(rèn)的填充字符是空格,對(duì)于數(shù)字類(lèi)型,默認(rèn)的對(duì)齊控制是右對(duì)齊,對(duì)于字符串,默認(rèn)的對(duì)齊控制是左對(duì)齊。

? sign、#0 用于數(shù)字的格式表達(dá)(都是可選),其中 sign 表示數(shù)字的符號(hào),用 “+” 表示在非負(fù)數(shù)前面添加一個(gè)+號(hào),用 “-” 表示在一個(gè)負(fù)數(shù)前添加一個(gè)負(fù)號(hào),空格 “ ” 表示在非負(fù)數(shù)前面添加一個(gè)空格字符,在負(fù)數(shù)前面添加一個(gè)負(fù)號(hào)。關(guān)于符號(hào)需要注意兩點(diǎn):首先添加負(fù)號(hào)對(duì)于負(fù)數(shù)是默認(rèn)行為,也就是說(shuō),即使不指定 sign 標(biāo)志,輸出負(fù)數(shù)時(shí)也會(huì)加一個(gè)負(fù)號(hào)。其次,如果數(shù)值是非負(fù)值,即使指定了 “-” 符號(hào)標(biāo)志,也會(huì)被忽略,同樣,對(duì)于負(fù)數(shù),即使指定了 “+” 符號(hào)標(biāo)志,輸出時(shí)也會(huì)用負(fù)號(hào)代替。

auto s0 = std::format("{0:},{0:+},{0:-},{0: }", 1);   //"1,+1,1, 1"
auto s1 = std::format("{0:},{0:+},{0:-},{0: }", -1);  //"-1,-1,-1,-1"

? # 用于轉(zhuǎn)換輸出數(shù)據(jù)的可替換形式,對(duì)于整數(shù)類(lèi)型,除了默認(rèn)的十進(jìn)制形式,還可以用二進(jìn)制、八進(jìn)制和十六進(jìn)制的形式表示數(shù)值??梢灾付?type 控制符與 # 配合的方式指定數(shù)值的表示形式,比如 type 控制字符 “d” 表示十進(jìn)制,這也是整數(shù)輸出的默認(rèn)形式,type 控制字符 “b” 表示二進(jìn)制,會(huì)在數(shù)值前插入“0b” 兩個(gè)字符。type 控制“o” 表示八進(jìn)制,會(huì)在數(shù)值前插入一個(gè)字符 “0”。type 控制“x” 表示十六進(jìn)制,會(huì)在數(shù)值前插入“0x” 兩個(gè)字符,如果用大寫(xiě)的 “X” 字符,則插入 “0X” 兩個(gè)字符。如果格式描述中指定了輸出域?qū)挘瑒t表示替換形式的字符要跟在域?qū)挃?shù)值后面,比如:

auto s = std::format("{0:#},{0:#6d},{0:#6b},{0:#6o},{0:#6x}", 10); //10,    10,0b1010,   012,   0xa

? 對(duì)于浮點(diǎn)數(shù)類(lèi)型,如果是有限數(shù)字,并且小數(shù)點(diǎn)后面沒(méi)有有效數(shù)字的情況,比如 6.0,默認(rèn)情況下是不輸出小數(shù)點(diǎn)的。如果希望強(qiáng)制輸出小數(shù)點(diǎn),則需要用到 # 控制符,還可以配合 “g” 和 “G” 轉(zhuǎn)換符在小數(shù)點(diǎn)后面補(bǔ)足 0,比如下面的代碼:

auto s = std::format("{0:},{0:#},{0:#6g},{0:#6G}", 6.0); //6,6.,6.00000,6.00000

? 0 表示在數(shù)字前填充前導(dǎo) 0,對(duì)于無(wú)窮大和無(wú)效值,則忽略這個(gè)符號(hào),不填充前導(dǎo) 0。如果 0 字符和對(duì)齊選項(xiàng)一起出現(xiàn),則忽略 0 字符??磶讉€(gè)例子:

auto s = std::format("{:+06d}", 12);   // s 的值為 "+00012"
auto s = std::format("{:#06x}", 10); // s 的值為 "0x000a"
auto s = std::format("{:<06}", -42);  // s 的值為 "-42   " (因 < 對(duì)齊忽略 0 )

? widthprecision 用于表示數(shù)字的域?qū)捄途?。width 就是一個(gè)正的十進(jìn)制數(shù)字,常用于配合對(duì)齊和填充控制符使用,也用于配合 0 控制符使用,前面已經(jīng)用展示了相關(guān)的例子。precision 的形式就是小數(shù)點(diǎn)后面跟一個(gè)十進(jìn)制數(shù)字或者跟一個(gè)嵌套的替換占位符表示。precision 只能用于浮點(diǎn)數(shù)或字符串,對(duì)于浮點(diǎn)數(shù)表示輸出的格式化精度,對(duì)于字符串則表示使用字符串中多少個(gè)字符。type 控制字符 “f” 常用來(lái)配合精度控制使用,來(lái)看幾個(gè)例子:

float pi = 3.14f;
auto s = std::format("{:10f}", pi);           // s = "  3.140000" (width = 10)
auto s = std::format("{:.5f}", pi);           // s = "3.14000" (precision = 5)
auto s = std::format("{:10.5f}", pi);         // s = "   3.14000"
auto s = std::format("{:>10.5}", "Kitty loves cats!");         // s = "     Kitty"

如果你覺(jué)得精度和寬度需要寫(xiě)死到格式化字符串中,會(huì)給使用帶來(lái)不便,那你就太小看 C++ 標(biāo)準(zhǔn)委員會(huì)的專(zhuān)家了。格式說(shuō)明部分支持嵌套格式化占位符的形式,動(dòng)態(tài)指定相關(guān)的格式化參數(shù),比如這樣:

auto s = std::format("{:{}f}", pi, 10);       // s = "  3.140000" (width = 10)
auto s = std::format("{:.{}f}", pi, 5);       // s = "3.14000" (precision = 5)
auto s = std::format("{:{}.{}f}", pi, 10, 5); // s = "   3.14000" (width = 10, precision = 5)

在上面幾行代碼中,參數(shù)列表中表示精度的 10 和 5 可以直接指定,也可以是通過(guò)其他形式計(jì)算得到的動(dòng)態(tài)結(jié)果,形式上非常靈活。但是使用嵌套定位符的形式,需要確保對(duì)應(yīng)的參數(shù)是正整數(shù)類(lèi)型,否則 std::format() 函數(shù)會(huì)拋出異常(C++ 23 會(huì)報(bào)編譯錯(cuò)誤,請(qǐng)參考資料 [9] 和 4.1 節(jié)的內(nèi)容)。

? L 控制符用于在格式化時(shí)引入地域化語(yǔ)言環(huán)境,這個(gè)控制符只用于算數(shù)類(lèi)型,比如整數(shù)、浮點(diǎn)數(shù)和布爾類(lèi)型數(shù)值的文本表示。對(duì)于整數(shù),可按照本地語(yǔ)言環(huán)境插入適當(dāng)?shù)臄?shù)位組分隔符。對(duì)于浮點(diǎn)數(shù),也是按照本地語(yǔ)言環(huán)境插入適當(dāng)?shù)臄?shù)位組和底分隔符。對(duì)于布爾類(lèi)型的文本表示,與使用 std::numpunct::truename 和 std::numpunct::falsename 得到的結(jié)果一致。西方表達(dá)數(shù)字的習(xí)慣是用 “,” 做數(shù)字分隔符,中文環(huán)境則沒(méi)有這個(gè)習(xí)慣,比如下面的代碼,將地域化環(huán)境切換為西方英語(yǔ)環(huán)境,則數(shù)字的格式就有差別了:

std::locale::global(std::locale("en_US"));  //將語(yǔ)言環(huán)境切換為西方英語(yǔ)環(huán)境
auto s = std::format("{0:12},{0:12L}", 432198409L); // s =    432198409, 432,198,409

? type 控制符用于確定數(shù)值以何種方式展現(xiàn),前面介紹 # 控制符的時(shí)候已經(jīng)介紹了 “o”、“b”、“d”、“x”、“X” 幾個(gè)控制符。其實(shí)還有很多控制字符,比如 “B”,作用和 “b” 一樣,只是數(shù)字前綴用 “0B” 兩個(gè)字符。“e” 和 “E” 是用指數(shù)形式展示浮點(diǎn)數(shù),“s” 用于輸出字符串,“a” 和 “A” 是用 16 進(jìn)制展示浮點(diǎn)數(shù)(用字母 p 表示指數(shù))等等。

2.3 自定義格式

? format 庫(kù)的強(qiáng)大之處還在于對(duì)用戶(hù)自定義類(lèi)型擴(kuò)展的支持,用戶(hù)可以通過(guò)提供 std::formatter<> 模板的特化實(shí)現(xiàn)對(duì)自定義類(lèi)型的支持。實(shí)際上,C++ 對(duì)標(biāo)準(zhǔn)庫(kù)中的類(lèi)型的支持,也是通過(guò)提供相應(yīng)的 std::formatter<> 模板特化實(shí)現(xiàn)的,比如 char 類(lèi)型的特化版本就是:

template<> struct formatter<char, char>;

如果要實(shí)現(xiàn)自定義數(shù)據(jù)類(lèi)型的格式化規(guī)則,需要針對(duì)自定義數(shù)據(jù)類(lèi)型實(shí)現(xiàn) std::formatter<> 的特化版本。具體的實(shí)現(xiàn)方法請(qǐng)參考《C++ 的 format 函數(shù)支持自定義類(lèi)型》。

3 std::vformat 和 std::format_args

? std::format() 與 std::vformat() 的關(guān)系就如同 sprintf() 和 vsprintf() 的關(guān)系一樣,主要用于用戶(hù)自定的帶格式化參數(shù)的函數(shù)與 format 庫(kù)配合使用的場(chǎng)景,而 std::format_args 則用來(lái)配合進(jìn)行參數(shù)傳遞??梢岳斫?,std::vformat() 就是 std::format() 的一個(gè)類(lèi)型擦除版本,為了配合 std::format() 的實(shí)現(xiàn)而存在,避免代碼都在 std::format() 中造成的模板膨脹問(wèn)題。一般不建議直接使用 std::vformat() 函數(shù),4.2 節(jié)也介紹了在 C++ 26 之前直接使用 std::vformat() 可能導(dǎo)致的問(wèn)題。但是在某些情況下,std::vformat() 函數(shù)還是有用武之地的,比如下面的例子。

? 在 C++ 提供函數(shù)參數(shù)包之前,可變參數(shù)函數(shù)如果要配合 sprintf() 將用戶(hù)輸出的變長(zhǎng)參數(shù)進(jìn)行格式化,就需要用 va_list 配合 vsnprintf() 函數(shù)實(shí)現(xiàn),比如這個(gè)記錄日志的函數(shù)就是典型的用法:

void __cdecl DebugTracing(int nLevel, const char* fmt, ... ) {
	if(nLevel >= g_Level) { //控制日志記錄的級(jí)別
		va_list args;
		va_start(args, fmt);
		int nBuf;
		char szBuffer[512];
		nBuf = _vsnprintf(szBuffer, sizeof(szBuffer)/sizeof(char), fmt, args);
		ASSERT(nBuf < sizeof(szBuffer)); 
		LogMessage(szBuffer); //日志寫(xiě)入系統(tǒng)
		va_end(args);
	}
}

因?yàn)?… 不是具體的參數(shù),所以無(wú)法直接調(diào)用 sprinf() 函數(shù)。只能用 va_start() 宏解析出 args 參數(shù),然后調(diào)用 vsprintf() 函數(shù)。這個(gè)函數(shù)中 szBuffer[] 的使用其實(shí)是讓人心驚膽戰(zhàn)的,512 個(gè)字節(jié)的數(shù)組放在棧中是個(gè)不妥的設(shè)計(jì),函數(shù)調(diào)用鏈層級(jí)比較深的話可能爆棧,此外,512 字節(jié)有時(shí)候可能還不夠,動(dòng)態(tài)申請(qǐng)內(nèi)存顯然很麻煩。

? 對(duì)于現(xiàn)代 C++ 而言,可以 std::format_args 參數(shù)配合 std::vformat() 函數(shù)安全地實(shí)現(xiàn)這個(gè)功能:

void DebugTracing(int nLevel, const std::string_view& fmt, std::format_args&& args) {
    if (nLevel >= g_Level) { //控制日志記錄的級(jí)別
        std::string msg = std::vformat(fmt, args);
        LogMessage(msg); //日志寫(xiě)入系統(tǒng)
    }
}

使用時(shí)可以借助 std::make_format_args() 函數(shù)幫忙構(gòu)造 args 參數(shù),比如:

DebugTracing(5, "{0:<d}{1:<x}", std::make_format_args(34, 42));

實(shí)際上,DebugTracing() 函數(shù)的使用可以借助函數(shù)參數(shù)包語(yǔ)法,將 args 替換成函數(shù)參數(shù)包,從而進(jìn)一步簡(jiǎn)化使用:

template <typename... Args>
void DebugTracing(int nLevel, const std::string_view& fmt, Args&&... args) {
    if (nLevel >= g_Level) { //控制日志記錄的級(jí)別
        std::string msg = std::vformat(fmt, std::make_format_args(args...));
        LogMessage(msg); //日志寫(xiě)入系統(tǒng)
    }
}

這樣使用的時(shí)候就不需要 std::make_format_args() 了:

DebugTracing(5, "{0:<d}{1:<x}", 34, 42);

4 C++ 23 和 26 的持續(xù)優(yōu)化

4.1 C++ 23 的改進(jìn)

? C++ 23 對(duì) format 庫(kù)的主要改進(jìn)是支持更多的標(biāo)準(zhǔn)庫(kù)類(lèi)型,比如 Ranges[5]、thread::id、std::stacktrace 等等。資料[6] 討論并明確了常見(jiàn)容器類(lèi)型的格式化輸出形式,比如:

  • map 類(lèi)型: {k1: v1, k2: v2}
  • set 類(lèi)型: {v1, v2}
  • 一般序列容器類(lèi)型: [v1, v2]

在 C++ 20 沒(méi)趕上火車(chē)的 std::print() 函數(shù)[4]也在 C++ 23 上車(chē)了,雖然還只支持標(biāo)準(zhǔn)輸出流和文件流,但是已經(jīng)具備了代替標(biāo)準(zhǔn)輸入輸出流的潛質(zhì)。還解決了時(shí)間庫(kù)的格式化在本地化處理時(shí)與 format 不兼容的問(wèn)題,這個(gè)在《C++ 的時(shí)間庫(kù)之八:format 與格式化》一篇已經(jīng)介紹過(guò)了。另外,當(dāng)使用 L 格式指定本地化輸出的時(shí)候,format 的結(jié)果采用何種編碼格式的問(wèn)題,也由資料[8] 明確了,就是采用 unicode 編碼格式,而不是系統(tǒng)默認(rèn)的格式。這很重要,以中文為例,Windows 系統(tǒng)默認(rèn)的格式是 GB2312、GB18030、GBK 等代表的擴(kuò)展 ASCII 編碼,而 Linux 采用的是 UTF-8 編碼,如果庫(kù)的規(guī)范在這個(gè)地方模糊,將給程序之間的數(shù)據(jù)交換帶來(lái)隱患。

? 資料 [9] 主要是對(duì) LEWG 提出的一些問(wèn)題進(jìn)行了改進(jìn),比如將格式化字符串的類(lèi)型從 basic_string_view 字符串類(lèi)型改為 basic_format_string<charT, Args…> 類(lèi)模板,改進(jìn)的好處可以用這個(gè)例子說(shuō)明一下:

auto s = std::format("{:d}", "I am not a number");

這行代碼在 C++ 20 版本中會(huì)在運(yùn)行時(shí)拋出一個(gè) std::format_error 異常,但是改進(jìn)后,就可以在編譯期檢查出參數(shù)類(lèi)型的不匹配,因?yàn)?basic_format_string 類(lèi)包含參數(shù)類(lèi)型信息,可以檢查格式化字符串的錯(cuò)誤,這大大提升了 format() 函數(shù)的安全性。

? 資料 [11] 的改進(jìn)值允許 format 支持非常量可格式化類(lèi)型。改進(jìn)的原因是在 C++ 20 中 format() 函數(shù)聲明大概是這個(gè)樣子:

template<class... Args>
string format(string_view fmt, const Args&... args);
//改進(jìn)后大概這樣:
template <class... Args>
string format(const format_string<_Types...> fmt, Args&&... args);

注意它對(duì)參數(shù)的要求是常量引用,也就是說(shuō),參數(shù)要么是帶有 const,要么是可拷貝類(lèi)型,這就限制了一些使用場(chǎng)景,比如非常量可迭代的 view,此時(shí)會(huì)產(chǎn)生臨時(shí)對(duì)象,這種隱含的臨時(shí)對(duì)象拷貝會(huì)產(chǎn)生不易覺(jué)察的開(kāi)銷(xiāo)。所以提案改進(jìn)的結(jié)果就是改用轉(zhuǎn)發(fā)引用,同時(shí)利用 format_string<> 對(duì)參數(shù)的生命周期進(jìn)行檢查。

? 資料 [12] 和 [13] 主要是對(duì)填充字符的容差和長(zhǎng)度估算問(wèn)題給出了明確的解決方案,這幾個(gè)問(wèn)題分別是 LWG issue 3576、LWG issue 3639 和 LWG issue 3780,感興趣的讀者可通過(guò) 資料 [12] 和 [13] 的問(wèn)題鏈接自行了解相關(guān)的情況。

4.2 C++ 26 的改進(jìn)

? C++ 26 的改進(jìn)也不少,資料 [14] 解決了 to_string() 函數(shù)和 format() 函數(shù)輸出數(shù)字的格式不一致問(wèn)題,調(diào)整后的 to_string() 函數(shù)輸出格式與 format() 函數(shù)的默認(rèn)格式一致。資料 [15] 引入了一個(gè)很有意思的問(wèn)題,來(lái)看這行代碼:

format("{:>{}}", "hello", "10")

按說(shuō) C++ 23 已經(jīng)支持對(duì)格式化字符串的檢查,以上錯(cuò)誤可以在編譯期報(bào)錯(cuò)的,但是實(shí)際上不是,上述代碼只是在運(yùn)行期產(chǎn)生了一個(gè) format_error 異常。我們?cè)?2.2 節(jié)介紹過(guò),format() 函數(shù)的格式化說(shuō)明支持嵌套的形式使用動(dòng)態(tài)格式參數(shù),這樣代碼就是指定了一個(gè)動(dòng)態(tài)寬度,按照順序,下一個(gè)參數(shù),也就是 “10” 就是這個(gè)動(dòng)態(tài)寬度,但是顯然,“10” 不是我們需要的整數(shù)類(lèi)型。實(shí)際上根據(jù)第一個(gè)參數(shù)是字符串類(lèi)型,編譯器應(yīng)該知道它的寬度是整數(shù)類(lèi)型,所以這里應(yīng)該能夠檢查出動(dòng)態(tài)寬度部分對(duì)應(yīng)的類(lèi)型不正確問(wèn)題。因此,P2757 提案就要求對(duì)格式化參數(shù)也進(jìn)行類(lèi)型檢查。

? 從 C++ 20 到 26,那么多數(shù)據(jù)類(lèi)型都被支持,怎么能少了指針呢?本質(zhì)上,指針作為整數(shù)類(lèi)型,可以有多種輸出格式,按照 16 進(jìn)制格式輸出就行了。但是,指針作為這么明確的一種數(shù)據(jù)類(lèi)型,每次都要被 cast 成整數(shù)類(lèi)型總是不爽。資料 [16] 就允許直接將指針格式化成地址形式:

//假設(shè) uintptr_t 是預(yù)定義的指針類(lèi)型
int i = 0;
format("{:#018x}", reinterpret_cast<uintptr_t>(&i)); // P2510 之前
format("{:018}", &i);// P2510 之后

能少敲幾次鍵盤(pán)也是極好的,對(duì)吧?

? 前面介紹過(guò),資料 [9] 提出了很多編譯期檢查的改進(jìn),但是對(duì)于字符串來(lái)說(shuō),由于資源限制,資料 [9] 沒(méi)有提供一個(gè)好的 API 來(lái)使用格式字符串在編譯期未知的格式化函數(shù)。作為一種變通方法,可以使用類(lèi)型擦除(type-erased) API(std::vformat() 函數(shù)),但是這嚴(yán)重破壞了運(yùn)行時(shí)的安全性。資料 [18] 建議引入 fmtlib 庫(kù)現(xiàn)成的 runtime_format 提供運(yùn)行期的檢查,以避免不安全的代碼破化系統(tǒng)安全性。資料 [9] 引入編譯期檢查的另一個(gè)問(wèn)題就是要求格式化字符串必須是編譯期可以求值的常量或立即函數(shù)的返回值,否則的話就會(huì)導(dǎo)致編譯錯(cuò)誤,比如:

std::string strfmt = translate("The answer is {}.");
auto s = std::format(strfmt, 42); // error

translate() 不是立即函數(shù)或常量函數(shù),所以 strfmt 不是編譯期常量,使用 format() 函數(shù)將導(dǎo)致編譯錯(cuò)誤。于是大家就想到了 vformat() 函數(shù),但是這個(gè)類(lèi)型擦除的 API 是為了避免模板膨脹而設(shè)計(jì)的,是給庫(kù)或格式化函數(shù)開(kāi)發(fā)者使用的。然而阿貓阿狗程序員朋友們被逼的沒(méi)辦法,也只能硬著頭皮用了。但是,用的不合適,錯(cuò)誤就來(lái)了,比如這個(gè)資料 [17] 上的例子:

std::string str = "{}";
std::filesystem::path path = "path/etic/experience";
auto args = std::make_format_args(path.string());
std::string msg = std::vformat(str, args);

這段看似“人畜無(wú)害”的代碼隱含著 UB,因?yàn)楦袷交瘏?shù)保存的是一個(gè)已經(jīng)銷(xiāo)毀的對(duì)象的引用。所以 [17] 建議將 make_format_args() 函數(shù)的參數(shù)從轉(zhuǎn)發(fā)引用改成左值引用,從而避免錯(cuò)誤使用右值的問(wèn)題。

? 資料 [19] 主要是解決 char 被當(dāng)成整數(shù)類(lèi)型處理的問(wèn)題,當(dāng) char 遇到 d 或 x 格式化描述符時(shí),會(huì)被當(dāng)成整數(shù)處理,但是 char 的符號(hào)性卻是由編譯器實(shí)現(xiàn)決定的,嗯,問(wèn)題就出來(lái)了。標(biāo)準(zhǔn) ASCII 都是正值,但是遇到 unicode 編碼的時(shí)候,就會(huì)遇到負(fù)值,這樣的情況不同的編譯器就會(huì)產(chǎn)生不一樣的輸出。所以資料 [19] 建議此時(shí)將 char 統(tǒng)一轉(zhuǎn)型為 unsigned char,從而避免不一致問(wèn)題。

? 資料 [20] 引入了許多標(biāo)準(zhǔn)庫(kù)類(lèi)型的格式化支持,其中包括對(duì)文件庫(kù)(filesystem)的 path 對(duì)象的格式化支持。但是因?yàn)閹Э崭竦淖址畷?huì)有 quoting 字符的問(wèn)題,那就是雙引號(hào),要不要轉(zhuǎn)義?還有就是本地化面臨的編碼問(wèn)題和格式問(wèn)題,一度被 SG 16 要求移除對(duì) path 的支持。資料 [21] 針對(duì)這些問(wèn)題提出了一種改進(jìn)的方案,總算解決了這個(gè)危機(jī)。

參考資料

1.https://stackoverflow.com/questions/2342162/stdstring-formatting-like-sprintf

[2] P0645R10: Text Formatting

[3] P2372R3: Fixing locale handling in chrono formatters

[4] P2093R14: Formatted output

[5] P2286R8: Formatting Ranges

[6] P2585R1: Improve default container formatting

[7] P2693R1: Formatting thread::id and stacktrace

[8] P2419R2: Clarify handling of encodings in localized formatting of chrono types

[9] P2216R3: std::format improvements

[10] P2508R1: Expose std::basic-format-string<charT, Args…>

[11] P2418R2: Add support for std::generator-like types to std::format

[12] P2572R1: std::format() fill character allowances

[13] P2675R1: format’s width estimation is too approximate and not forward compatible

[14] P2587R3: to_string or not to_string

[15] P2757R3: Type-checking format args

[16] P2510R3: Formatting pointers

[17] P2905R2: Runtime format strings

[18] P2918R2: Runtime format strings II

[19] P2909R4: Fix formatting of code units as integers (Dude, where’s my char?)

[20] P1636R2: Formatters for librarytypes

[21] P2845R8: Formatting of std::filesystem::path

到此這篇關(guān)于C++ 的 format 和 vformat 函數(shù)的文章就介紹到這了,更多相關(guān)C++ format 和 vformat 函數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論