C++20 std::format part 2:其他的函式

延續前面的 part 1,這邊繼續來講一些 C++20 std::format 的其他東西吧~
這邊,主要是來講一下他提供的一些其他函式。


輸出到既有物件/記憶體

std::format 所提供的函式,最基本的應該就是前面已經介紹過的 std::format();他的用法基本上就是會產生一個 std::string、然後回傳出來。

而在某些狀況下,可能會需要他把資料寫到既有的變數/記憶體中,而不是建立一個新的物件,這時候,就可以使用 std::format_to()文件)或 std::format_to_n()文件)來做到。

std::format_to() 的使用方法基本上和 std::format() 相同,只是在呼叫函式的時候,要多加一個要輸出的記憶體空間的 iterator(要符合 std::output_iterator 這個 concept、文件);而函式執行結束時,則會回傳一個同型別的 iterator、代表之後要寫入的位置。

下面就是一個簡單的例子:

std::string s2 = "There are ";
std::format_to(std::back_inserter(s2), "{} books", 10);
// There are 10 books

上面的例子,就是透過 std::format_to() 將格式化後的資料,附加輸出到 s2 這個字串的後面。

而使用 std::back_inserter()文件)則是用來針對 s2 這個字串產生一個 std::back_insert_iterator,讓他透過 push_back() 來把資料放進去。

而如果是想要把結果放到前面,則也可以透過 std::inserter()文件)來做到;下面就是一個例子:

std::string s2 = "There are ";
std::format_to(std::inserter(s2,s2.begin()), "{} books", 10);
// 10 booksThere are

由於 std::inserter() 第二個參數是用來指定開始插入的位置,所以理論上可以從任何地方開始插入要輸出的內容。

如果想要透過 std::format_to() 把結果寫到 output stream 的話呢,除了透過 operator<< 外,也可以透過 std::ostream_iterator<>()文件)來產生需要的 iterator。

下面就是一個直接輸出到 standard output 的簡單例子:

std::format_to(std::ostream_iterator<char>(std::cout), "{} books", 10);

如果要輸出到其他的 output stream 的話(例如 std::ofstream),基本上也都可以這樣使用。

不過實際上,個人是覺得直接 std::format()operator<< 應該比較方便就是了。 ^^”


如果是要寫到已經配置好的記憶體空間的話,則可以寫成下面的形式:

std::string s3 = "There are 5 books";
std::format_to(s3.begin(), "{} books", 10);
// 10 bookse 5 books

但是這樣寫的話,要注意的就是要寫入的空間是否夠大了!因為雖然這邊是使用 std::string 來做輸出目標,但是由於他是把它當作一般的 iterator 來做操作,所以當空間不夠的時候,他並不會自動去增加大小,在 debug 模式是會直接當掉。也因此,在使用的時候要注意空間大小是否夠用。

也因為他是直接透過 iterator 來操作,所以這邊也可以透過 rbegin() 來逆向輸出,感覺還算滿有趣的~

而針對傳統 C 的字串,也是可以使用 std::format_to() 來寫入的,但是除了要注意字串長度是否夠用外,也要記得自己加上結尾的「\0」了~下面就是一個簡單的例子:

char s4[10];
char* end = std::format_to(s4, "{} books", 10);
*end = '\0';
// 10 books

由於要寫到已經配置好的記憶體空間時會需要確保大小、以避免出問題,所以實際上 std::format 也還有提供 std::format_to_n() 這個函式,可以用來控制輸出的數量。

下面就是一個簡單的例子:

std::string s = "123456789";
std::format_to_n(s.begin(), s.length(), "{}", 1234567890);
// 123456789

std::format_to(s.begin(), "{}", 1234567890);
// exception

在呼叫 std::format_to_n() 的時候,會需要指定要輸出的最大長度,超過地都部分都會被切掉;所以在上面的例子裡面,結果就會變成剩下「123456789」 、最後的「0」會直接被省略。

而如果是使用 std::format_to() 的話,這個情況下執行就會出問題。


如果需要像是在使用 snprintf() 的時候一樣,可以先取得格式化後的大小、然後用來配置記憶體空間以作為輸出之用呢?這邊也有提供 formatted_size() 這個函式,可以用來取得需要的大小。

這邊的寫法大概會像下面這樣:

int iNum = 10;
std::string sFMT = "{} books.";

size_t uSize = std::formatted_size(sFMT, iNum);
char* sText = new char[uSize + 1];
char* pEnd = std::format_to(sText, sFMT, iNum);
*pEnd = '\0';

std::cout << sText << std::endl;

delete[] sText;

不過老實說,個人覺得這邊直接用標準的 std::format() 讓他產生 std::string 還比較方便,應該沒必要搞成這樣。


非 Template 版本

上面講的函式基本上都是透過 function parameter pack 來實作的,而他其實也還有 vformat()vformat_to() 這種不是 template 形式的函式,可以搭配 format_args 來使用。

這種函式應該算是他內部實作用的?實際上在要使用時,也是要透過 std::make_format_args()文件)來將要輸出的引數轉換成 format_args 的形式來做傳遞。

下面是一個使用範例:

#include <format>
#include <iostream>
#include <string_view>
 
void wlog(std::string_view users_fmt, std::format_args&& args) {
  static int n{};
  std::clog << std::format("{:04} : ", n++)
   << std::vformat(users_fmt, args) << '\n';
}
 
int main()
{
  wlog("{:02} | {} | {}", std::make_format_args(1, 2.0, "3"));
}

不過老實說,個人比較不確定這東西到底該怎麼應用,所以這邊就先不細究了。


錯誤處理

在 Heresy 個人來看,C++20 的 std::format 和 {fmt} 最大的不同,應該就在於兩者在錯誤處理上的不同了。

C++20 的 format 除了語法上的錯誤之外,其他的錯誤都是在執行階段,透過例外(exception)來做回報;而他的型別會是 std::format_error文件)。

而要做錯誤處理的話,基本上就是用 try and catch 的形式來做了~下面就是一個簡單的例子:

try
{
  std::format("{:d}", "Hello");
}
catch (std::format_error& e)
{
  std::cerr << e.what() << '\n';
  // invalid string type
}

在上面的例子裡面,由於前面的格式化字串中是指定型別是整數專用的「d」,但是後面給的變數卻是一個字串,所以在試圖產生格式化文字的時候,就會出現錯誤而無法完成。

另外要注意的是,感覺在使用 std::format_to() 時如果要寫的資料比配置好的記憶體空間大的話,似乎是不會丟出例外的。


{fmt} 感覺上應該是最大限度地在編譯階段就進行檢查,所以很多錯誤都會在編譯的時候就回報;像是下面這行程式:

fmt::format("{:d}", "Hello");

這樣的程式,在 {fmt} 的話會在編譯階段就出現錯誤、而無法完成編譯;基本上算是達到了盡早發現問題的目的。

但是相對地,它的代價就是格式化的字串也需要在編譯階段就決定好,不能到了執行階段再產生,這點也算是喪失了一些使用上的彈性。


本系列目錄:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。