C++ 20 的 std::format 是一個很神奇、很實用的工具,最神奇的地方在於它能在編譯期檢查字元串的格式是否正確,而且不需要什麼特殊的使用方法,只需要像使用普通函數那樣傳參即可。 #include <format> int a = 1; std::string s1 = std::form ...
C++ 20 的 std::format
是一個很神奇、很實用的工具,最神奇的地方在於它能在編譯期檢查字元串的格式是否正確,而且不需要什麼特殊的使用方法,只需要像使用普通函數那樣傳參即可。
#include <format>
int a = 1;
std::string s1 = std::format("a: {}", a); // OK
std::string s2 = std::format("a: {}, b: {}", a); // 編譯錯誤
C++ 20 的 std::format
來自一個著名的開源庫 {fmt}。在 C++ 20 之前,fmt 需要為每個字元串字面量創建不同的類型才能實現編譯期格式檢查。fmt 提供了一個 FMT_STRING
巨集以簡化使用的流程。
#include <fmt/format.h>
int a = 1;
std::string s1 = fmt::format(FMT_STRING("a: {}"), a); // OK
std::string s2 = 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
語句不能在編譯期求值,因此會引發編譯錯誤,從而實現了在編譯期檢查出字元串的格式錯誤。
namespace my {
template<class ...Args>
class format_string {
private:
std::string_view str;
public:
template<class T>
requires std::convertible_to<const T &, std::string_view>
consteval format_string(const T &s)
: str(s)
{
std::size_t actual_num = 0;
for (std::size_t i = 0; i + 1 < str.length(); i++) {
if (str[i] == '{' && str[i + 1] == '}') {
actual_num++;
}
}
constexpr std::size_t expected_num = sizeof...(Args);
if (actual_num != expected_num) {
throw std::format_error("incorrect format string");
}
}
std::string_view get() const { return str; }
};
template<class ...Args>
std::string format(format_string<std::type_identity_t<Args>...> fmt, Args &&...args) {
// 省略具體的格式化邏輯
}
}
有一個細節,此處 format
函數的參數寫的是 format_string<std::type_identity_t<Args>...>
,直接寫 format_string<Args ...>
是無法隱式轉換的,因為模板實參推導 (template argument deduction) 不會考慮隱式轉換,C++ 20 提供了一個工具 std::type_identity
可以解決這個問題。std::type_identity
其實就是一個關於類型的恆等函數,但是這麼倒騰一下就能在模板實參推導中建立非推導語境 (non-deduced context),進而正常地匹配到隱式轉換,C++ 就是這麼奇怪。參考資料:c++ - why would type_identity make a difference? - Stack Overflow