字串格式化漫談
前言
春風化雨,萬物復甦,程式設計始於Hello world
,C 語言的 Hello world 可如下,同樣 C++ 亦可如下:
#include <stdio.h> int main() { printf("Hello World!"); return 0; }
printf 函式型別為:
int printf(const char *fmt,...);
據此我們可以格式化輸出:
#include <stdio.h> int main() { const char *name="Tony Stark"; printf("Hello %s\n",name); return 0; }
使用 C 編譯器 Clang/MSVC/GCC 將其編譯執行,在終端或者命令列中會輸出如下結果:
$ Hello Tony Stark
printf 函式家族宣告如下:
// https://en.cppreference.com/w/cpp/header/cstdio int printf( const char* format, ... ); int fprintf(std::FILE* stream, const char* format, ... ); int sprintf( char* buffer, const char* format, ... ); int snprintf( char* buffer, std::size_t buf_size, const char* format, ... ); int vprintf( const char* format, va_list vlist ); int vfprintf( std::FILE* stream, const char* format, va_list vlist ); int vsprintf( char* buffer, const char* format, va_list vlist ); int vsnprintf( char* buffer, std::size_t buf_size, const char* format, va_list vlist );
如果我們需要格式化字串時,則可以使用sprintf
或者是snprintf
,那麼問題來了,snprintf
是如何格式化的?
格式化內幕
要了解 snprintf 的細節,我們需要去找一個 libc 瞭解一番,這裡建議是musl
,musl 只支援 Linux,沒有像 Glibc 那麼多的遺留程式碼,程式碼比較整潔。而 Visual C++ 的 ucrt 原始碼基本使用 C++ 模板編寫,比較複雜,不容易藉此說清 snprintf 的細節。在 musl snprintf.c 原始碼中,snprintf 將會呼叫vsnprintf
,
// https://github.com/bminor/musl/blob/master/src/stdio/snprintf.c #include <stdio.h> #include <stdarg.h> int snprintf(char *restrict s, size_t n, const char *restrict fmt, ...) { int ret; va_list ap; va_start(ap, fmt); ret = vsnprintf(s, n, fmt, ap); va_end(ap); return ret; } // https://github.com/bminor/musl/blob/master/src/stdio/vsnprintf.c int vsnprintf(char *restrict s, size_t n, const char *restrict fmt, va_list ap) { unsigned char buf[1]; char dummy[1]; struct cookie c = { .s = n ? s : dummy, .n = n ? n-1 : 0 }; FILE f = { .lbf = EOF, .write = sn_write, .lock = -1, .buf = buf, .cookie = &c, }; if (n > INT_MAX) { errno = EOVERFLOW; return -1; } *c.s = 0; return vfprintf(&f, fmt, ap); }
在 musl 之中,vsnprintf 建立一個 FILE 結構,使用 vsnprintf 格式化字串。這裡使用了va_list
va_start
va_end
巨集,這組巨集將變參函式的引數從函式棧中取出來,從而實現了變參函式的功能,在va_*
musl 和 reactos (AMD64) 中實現分別如下:
// https://github.com/bminor/musl/blob/master/include/stdarg.h #define va_start(v,l)__builtin_va_start(v,l) #define va_end(v)__builtin_va_end(v) #define va_arg(v,l)__builtin_va_arg(v,l) #define va_copy(d,s)__builtin_va_copy(d,s) // https://github.com/reactos/reactos/blob/master/sdk/include/crt/vadefs.h // AMD64 #define _PTRSIZEOF(n) ((sizeof(n) + sizeof(void*) - 1) & ~(sizeof(void*) - 1)) #define _ISSTRUCT(t) ((sizeof(t) > sizeof(void*)) || (sizeof(t) & (sizeof(t)-1)) != 0) #define _crt_va_start(v,l)((void)((v) = (va_list)_ADDRESSOF(l) + _PTRSIZEOF(l))) #define _crt_va_arg(v,t)(_ISSTRUCT(t) ? \ (**(t**)(((v) += sizeof(void*)) - sizeof(void*))) : \ (*(t*)(((v) += sizeof(void*)) - sizeof(void*)))) #define _crt_va_end(v)((void)((v) = (va_list)0)) #define __va_copy(d,s)((void)((d) = (s)))
vfprintf
函式的原始碼在:musl: src/stdio/vfprintf.c
。我們可以發現格式化輸出實際上是結寫format
,在解析到佔位符時,使用va_arg
提取va_list
中的變數(或者轉變為特定格式字串後)替換佔位符,從而實現格式化輸出的目的(檔案或者緩衝區)。格式化佔位符以%
開頭,支援格式化的型別可以參考http://pubs.opengroup.org/onlinepubs/9699919799/functions/fprintf.html
瞭解到格式化輸出的原理之後,我們可以很容易的實現一個格式化字串或者格式化輸出函式,在 nginx 原始碼中,就有一個
ngx_vslprintf
很容易移植。
C-Style 格式化的缺陷
上述格式化使用變參函式,在 C++ 中是C-Style format ,這種使用 va_list 的函式在 C++ 中是不被推薦的F.55: Don’t use va_arg arguments ,可能是程式設計師疏忽或者故意,依賴 va_list 的程式碼很容易由於型別不匹配,引數個數不匹配而出現溢位。導致安全漏洞或者是程式崩潰。以下程式碼可能會導致程式崩潰,並且編譯器也不會警告:
#include <string_view> #include <cstdio> #include <cstdarg> int dump(const char *fmt,...){ int ret; va_list ap; va_start(ap,fmt); vfprintf(stderr,fmt,ap); va_end(ap); return ret; } int main(){ std::string_view name="Tony Stark"; dump("hello %s\n",name); return 0; }
編譯器的資訊為:
使用內建 specs。 COLLECT_GCC=/opt/gcc/bin/g++ COLLECT_LTO_WRAPPER=/opt/gcc/libexec/gcc/x86_64-linux-gnu/9.0.1/lto-wrapper 目標:x86_64-linux-gnu 配置為:../configure --with-pkgversion=Baslat.Inc --prefix=/opt/gcc --enable-shared --enable-linker-build-id --without-included-gettext --enable-threads=posix --enable-checking=release --enable-languages=c,c++ --disable-multilib --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --with-abi=m64 --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu 執行緒模型:posix gcc 版本 9.0.1 20190426 (prerelease) (Baslat.Inc)
執行編譯器命令:
/opt/gcc/bin/g++ -fsanitize=address -fno-omit-frame-pointer fuck.cc -std=c++17
執行程式:
./a.out
地址消毒劑報告如下:
AddressSanitizer:DEADLYSIGNAL ================================================================= ==16509==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000000a (pc 0x7fc137bc3af2 bp 0x7ffffbf14ea0 sp 0x7ffffbf145c8 T0) ==16509==The signal is caused by a READ memory access. ==16509==Hint: address points to the zero page. #0 0x7fc137bc3af1(/usr/lib/x86_64-linux-gnu/libasan.so.5+0x109af1) #1 0x7fc137b0e61c(/usr/lib/x86_64-linux-gnu/libasan.so.5+0x5461c) #2 0x7fc137b0efb4 in __interceptor_vfprintf (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x54fb4) #3 0x400cf0 in dump(char const*, ...) (/tmp/a.out+0x400cf0) #4 0x400e06 in main (/tmp/a.out+0x400e06) #5 0x7fc136dabb96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96) #6 0x400b09 in _start (/tmp/a.out+0x400b09) AddressSanitizer can not provide additional info. SUMMARY: AddressSanitizer: SEGV (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x109af1) ==16509==ABORTING
其實很容易理解,由於 std::string_view 的結構大致如下:
template<class charT, class traits = char_traits<charT>> class basic_string_view { public: // types typedef traits traits_type; typedef charT value_type; typedef charT* pointer; typedef const charT* const_pointer; typedef charT& reference; typedef const charT& const_reference; typedef implementation-defined const_iterator; typedef const_iterator iterator; typedef reverse_iterator<const_iterator> const_reverse_iterator; typedef const_reverse_iterator reverse_iterator; typedef size_t size_type; typedef ptrdiff_t difference_type; static constexpr size_type npos = size_type(-1); // 7.3, basic_string_view constructors and assignment operators constexpr basic_string_view() noexcept; constexpr basic_string_view(const basic_string_view&) noexcept = default; basic_string_view& operator=(const basic_string_view&) noexcept = default; template<class Allocator> constexpr basic_string_view(const charT* str); constexpr basic_string_view(const charT* str, size_type len); // 7.4, basic_string_view iterator support constexpr const_iterator begin() const noexcept; constexpr const_iterator end() const noexcept; constexpr const_iterator cbegin() const noexcept; constexpr const_iterator cend() const noexcept; const_reverse_iterator rbegin() const noexcept; const_reverse_iterator rend() const noexcept; const_reverse_iterator crbegin() const noexcept; const_reverse_iterator crend() const noexcept; // 7.5, basic_string_view capacity constexpr size_type size() const noexcept; constexpr size_type length() const noexcept; constexpr size_type max_size() const noexcept; constexpr bool empty() const noexcept; // 7.6, basic_string_view element access constexpr const_reference operator[](size_type pos) const; constexpr const_reference at(size_type pos) const; constexpr const_reference front() const; constexpr const_reference back() const; constexpr const_pointer data() const noexcept; // 7.7, basic_string_view modifiers constexpr void remove_prefix(size_type n); constexpr void remove_suffix(size_type n); constexpr void swap(basic_string_view& s) noexcept; size_type copy(charT* s, size_type n, size_type pos = 0) const; constexpr basic_string_view substr(size_type pos = 0, size_type n = npos) const; constexpr int compare(basic_string_view s) const noexcept; constexpr int compare(size_type pos1, size_type n1, basic_string_view s) const; constexpr int compare(size_type pos1, size_type n1, basic_string_view s, size_type pos2, size_type n2) const; constexpr int compare(const charT* s) const; constexpr int compare(size_type pos1, size_type n1, const charT* s) const; constexpr int compare(size_type pos1, size_type n1, const charT* s, size_type n2) const; constexpr size_type find(basic_string_view s, size_type pos = 0) const noexcept; constexpr size_type find(charT c, size_type pos = 0) const noexcept; constexpr size_type find(const charT* s, size_type pos, size_type n) const; constexpr size_type find(const charT* s, size_type pos = 0) const; constexpr size_type rfind(basic_string_view s, size_type pos = npos) const noexcept; constexpr size_type rfind(charT c, size_type pos = npos) const noexcept; constexpr size_type rfind(const charT* s, size_type pos, size_type n) const; constexpr size_type rfind(const charT* s, size_type pos = npos) const; constexpr size_type find_first_of(basic_string_view s, size_type pos = 0) const noexcept; constexpr size_type find_first_of(charT c, size_type pos = 0) const noexcept; constexpr size_type find_first_of(const charT* s, size_type pos, size_type n) const; constexpr size_type find_first_of(const charT* s, size_type pos = 0) const; constexpr size_type find_last_of(basic_string_view s, size_type pos = npos) const noexcept; constexpr size_type find_last_of(charT c, size_type pos = npos) const noexcept; constexpr size_type find_last_of(const charT* s, size_type pos, size_type n) const; constexpr size_type find_last_of(const charT* s, size_type pos = npos) const; constexpr size_type find_first_not_of(basic_string_view s, size_type pos = 0) const noexcept; constexpr size_type find_first_not_of(charT c, size_type pos = 0) const noexcept; constexpr size_type find_first_not_of(const charT* s, size_type pos, size_type n) const; constexpr size_type find_first_not_of(const charT* s, size_type pos = 0) const; constexpr size_type find_last_not_of(basic_string_view s, size_type pos = npos) const noexcept; constexpr size_type find_last_not_of(charT c, size_type pos = npos) const noexcept; constexpr size_type find_last_not_of(const charT* s, size_type pos, size_type n) const; constexpr size_type find_last_not_of(const charT* s, size_type pos = npos) const; constexpr bool starts_with(basic_string_view s) const noexcept; // C++2a constexpr bool starts_with(charT c) const noexcept;// C++2a constexpr bool starts_with(const charT* s) const;// C++2a constexpr bool ends_with(basic_string_view s) const noexcept;// C++2a constexpr bool ends_with(charT c) const noexcept;// C++2a constexpr bool ends_with(const charT* s) const;// C++2a private: const_pointer data_;// exposition only size_typesize_;// exposition only };
由於 string_view 值被錯誤的轉變為char *
,而 C-Style 的字串是null-terminated string
,在處理%s
的時候就容易出現溢位而導致程式崩潰,同樣,如果型別寬度不一致,比如 format 中需要%lld
,而輸入為int
同樣容易出現問題。但這種問題可能由於對齊的緣故而不容易導致程式崩潰。
在這個例子中,如果將std::string_view
改成std::string
, clang 8.0.1 則會報告 std::string 不是 POD 型別的錯誤:
fuck2.cc:16:21: error: cannot pass object of non-trivial type 'std::string' (aka 'basic_string<char>') through variadic function; call will abort at runtime [-Wnon-pod-varargs] dump("hello %s\n",name); ^ 1 error generated.
安全格式化解決方案
編譯器的引數匹配檢查
為了減少上面錯誤的發生,開發者增加了很多解決方案,比如,對於 C 或者 C++ 而言,可以使用特定的Attributes
限制函式的屬性,當格式不匹配時,編譯器會發出警告。
#ifdef __GNUC__ void log_unlocked(int level, const char *fmt, ...) __attribute__((__format__(__printf__, 2, 3))); #else void log_unlocked(int level, const char *fmt, ...); #endif
但這種方案受限於編譯器,不是一個普遍方案。Clang
保持了對 GCC 的相容,可以使用上述__attribute__
。而 MSVC 則可以使用
SAL:_Printf_format_string_
。
變參函式模板的輔助
如果要在 C++ 當中使用類似的格式化函式方案,可以變參函式模板包裝,cppwinrt 的作者 Kenny Kerr 就有一篇文章告訴人們改怎麼做:Windows with C++ - Using Printf with Modern C++ 。
template <typename T> T Argument(T value) noexcept { return value; } template <typename T> T const *Argument(std::basic_string<T> const &value) noexcept { return value.c_str(); } template <typename... Args> int StringPrint(char *const buffer, size_t const bufferCount, char const *const format, Args const &... args) noexcept { int const result = snprintf(buffer, bufferCount, format, Argument(args)...); return result; } template <typename... Args> std::string StrFormat(const char *format, Args... args) { std::string buffer; size_t const size = StringPrint(nullptr, 0, format, args...); buffer.resize(size); StringPrint(&buffer[0], buffer.size() + 1, format, args...); return buffer; }
在格式化時將std::string
轉變為了const char *
,這種方案的缺陷有兩處,第一std::string
是可以存在\0
這樣的字元的,但是在這裡卻被截斷,第二,std::string
需要再次計算長度。
現代 C++ 格式化庫
既然 C-Style 的格式化方案缺陷那麼多,C++ 開發者們也會不遺餘力的去造輪子,實現自己的目標的,比如我剛學 C++ 那會,都是iostream
家族,字串格式化可以使用stringstream
,iostream
這類方案一直被視為糟糕的設計,一來繼承複雜,二來效率低。特別在 C++11 變參模板等特性出來後,飽受鄙視,因此也不是建議的格式化方案。限制 C++20 都快釋出了,好的格式化庫有積極入標準的fmtlib
。而 Facebook 的 C++ 標準庫補充folly
也有一個format
實現。在 Google 裡面,很多專案使用了 C++,他們也積累了一些 C++ 元件,後來開源了Abseil
,Abseil 中也有字串格式化函式absl::StrFormat
,absl::StrAppendFormat
還有一些實現比較慢,程式碼不太乾淨的,這裡也就不多說了。
積極入標準的 fmtlib
fmtlib 目前是衝著進入 C++ 標準去的,它支援兩種風格,一個是類似 python 的風格:
fmt::format("The answer is {}.", 42); fmt::print("I'd rather be {1} than {0}.", "right", "happy");
通過過載format_to
函式, 還可以輸出特定的物件。
fmtlib 還支援 printf 的格式化風格,但是是格式化安全的。
std::string message = fmt::sprintf("The answer is %d", 42);
當型別不匹配時會丟擲異常,這是一種執行時行為。另外 fmtlib 還支援wchar_t
,這在Windows
系統中比較重要。在格式化浮點型別時,可能會回退到snprintf
。
Facebook folly format
Folly format 的風格類似於 python 的格式化風格,與 fmtlib 的第一種一致。
using folly::format; using folly::sformat; using folly::vformat; using folly::svformat; // Objects produced by format() can be streamed without creating // an intermediary string; {} yields the next argument using default // formatting. std::cout << format("The answers are {} and {}", 23, 42); // => "The answers are 23 and 42" // If you just want the string, though, you're covered. std::string result = sformat("The answers are {} and {}", 23, 42); // => "The answers are 23 and 42" // To insert a literal '{' or '}', just double it. std::cout << format("{}}", 23, 42); // => "23 {} {42}" // Arguments can be referenced out of order, even multiple times std::cout << format("The answers are {1}, {0}, and {1} again", 23, 42); // => "The answers are 42, 23, and 42 again" // It's perfectly fine to not reference all arguments std::cout << format("The only answer is {1}", 23, 42); // => "The only answer is 42" // Values can be extracted from indexable containers // (random-access sequences and integral-keyed maps), and also from // string-keyed maps std::vector<int> v {23, 42}; std::map<std::string, std::string> m { {"what", "answer"} }; std::cout << format("The only {1[what]} is {0[1]}", v, m); // => "The only answer is 42" // If you only have one container argument, vformat makes the syntax simpler std::map<std::string, std::string> m { {"what", "answer"}, {"value", "42"} }; std::cout << vformat("The only {what} is {value}", m); // => "The only answer is 42" // same as std::cout << format("The only {0[what]} is {0[value]}", m); // => "The only answer is 42" // And if you just want the string, std::string result = svformat("The only {what} is {value}", m); // => "The only answer is 42" std::string result = sformat("The only {0[what]} is {0[value]}", m); // => "The only answer is 42" // {} works for vformat too std::vector<int> v {42, 23}; std::cout << vformat("{} {}", v); // => "42 23" // format and vformat work with pairs and tuples std::tuple<int, std::string, int> t {42, "hello", 23}; std::cout << vformat("{0} {2} {1}", t); // => "42 23 hello" // Format supports width, alignment, arbitrary fill, and various // format specifiers, with meanings similar to printf // "X<10": fill with 'X', left-align ('<'), width 10 std::cout << format("{:X<10} {}", "hello", "world"); // => "helloXXXXX world" // Field width may be a runtime value rather than part of the format string int x = 6; std::cout << format("{:-^*}", x, "hi"); // => "--hi--" // Explicit arguments work with dynamic field width, as long as indexes are // given for both the value and the field width. std::cout << format("{2:+^*0}", 9, "unused", 456); // => "+++456+++" // Format supports printf-style format specifiers std::cout << format("{0:05d} decimal = {0:04x} hex", 42); // => "00042 decimal = 002a hex" // Formatter objects may be written to a string using folly::to or // folly::toAppend (see folly/Conv.h), or by calling their appendTo(), // str(), and fbstr() methods std::string s = format("The only answer is {}", 42).str(); std::cout << s; // => "The only answer is 42" // Decimal precision usage std::cout << format("Only 2 decimals is {:.2f}", 23.34134534535); // => "Only 2 decimals is 23.34"
但 Folly 的構建是個重量級的活動,所以像我這樣的開發者一般是不會採用 folly 的,雖然 folly 在 Linux 上推出,但目前可以構建為 Windows x64,使用 vcpkg 亦可安裝。
Google Abseil StrFormat
在 GNK 專案中,我曾使用 fmtlib,但 clang-tidy 老是警告沒有捕獲異常,後來加了捕獲異常,在考察 Abseil 之後,我就將其切換到 Abseil 了。
Abseil StrFormat 只支援 C-Style 的格式化風格,格式化支援的型別可以檢視如下注釋:
// In specific, the `FormatSpec` supports the following type specifiers: //* `c` for characters //* `s` for strings //* `d` or `i` for integers //* `o` for unsigned integer conversions into octal //* `x` or `X` for unsigned integer conversions into hex //* `u` for unsigned integers //* `f` or `F` for floating point values into decimal notation //* `e` or `E` for floating point values into exponential notation //* `a` or `A` for floating point values into hex exponential notation //* `g` or `G` for floating point values into decimal or exponential //notation based on their precision //* `p` for pointer address values //* `n` for the special case of writing out the number of characters //written to this point. The resulting value must be captured within an //`absl::FormatCountCapture` type. // // NOTE: `o`, `x\X` and `u` will convert signed values to their unsigned // counterpart before formatting. // // Examples: //"%c", 'a'-> "a" //"%c", 32-> " " //"%s", "C"-> "C" //"%s", std::string("C++") -> "C++" //"%d", -10-> "-10" //"%o", 10-> "12" //"%x", 16-> "10" //"%f", 123456789-> "123456789.000000" //"%e", .01-> "1.00000e-2" //"%a", -3.0-> "-0x1.8p+1" //"%g", .01-> "1e-2" //"%p", *int-> "0x7ffdeb6ad2a4" // //int n = 0; //std::string s = absl::StrFormat( //"%s%d%n", "hello", 123, absl::FormatCountCapture(&n)); //EXPECT_EQ(8, n); // // The `FormatSpec` intrinsically supports all of these fundamental C++ types: // // *Characters: `char`, `signed char`, `unsigned char` // *Integers: `int`, `short`, `unsigned short`, `unsigned`, `long`, //`unsigned long`, `long long`, `unsigned long long` // *Floating-point: `float`, `double`, `long double` // // However, in the `str_format` library, a format conversion specifies a broader // C++ conceptual category instead of an exact type. For example, `%s` binds to // any string-like argument, so `std::string`, `absl::string_view`, and // `const char*` are all accepted. Likewise, `%d` accepts any integer-like // argument, etc.
Abseil 支援如下函式:
- StrFormat
- StrAppendFormat
- StreamFormat
- PrintF
- FPrintF
- SNPrintF
我們在實現日誌庫時,可以使用StrFormat
格式化日誌級別,時間等資訊,然後使用StrAppendFormat
格式化日誌內容,這比 fmtlib 要方便的多。Abseil StrFormat 使用編譯期檢查取代執行時異常,這是讓我選擇的主要原因。配合absl::string_view
在 GNK 一個 C++14 專案中,C++17 的使用體驗非常好,字串記憶體分配也減少了很多。
如果在 Windows 環境wchar_t
編碼環境使用 Abseil 可能效果還不如 fmtlib。由於實現了編譯期型別檢查,程式碼還是比較複雜,如果要將 Abseil StrFormat 剝離出來還比較麻煩。
字串去格式化
如果我們使用字串連線取代字串格式化,字串格式化問題則會少很多。在 Abseil 之中,有 StrCat 和 Subsitute 方案,可以連線或者組裝字串。
Substitute
實際上Subsitute 類似 python 格式化風格,但引數只支援 0~9 個:
auto s=Substitute("$1 purchased $0 $2. Thanks $1!", 5, "Bob", "Apples");
實現 Subsitute 的關鍵是遍歷格式化引數,解析到$
後獲得引數位置,然後將特定的引數轉變為Arg
,Arg 型別過載支援不同的基本型別,以及字串型別,將其轉變為absl::string_view
陣列,然後由SubstituteAndAppendArray
拼接在一起。
class Arg { public: // Overloads for std::string-y things // // Explicitly overload `const char*` so the compiler doesn't cast to `bool`. Arg(const char* value)// NOLINT(runtime/explicit) : piece_(absl::NullSafeStringView(value)) {} template <typename Allocator> Arg(// NOLINT const std::basic_string<char, std::char_traits<char>, Allocator>& value) noexcept : piece_(value) {} Arg(absl::string_view value)// NOLINT(runtime/explicit) : piece_(value) {} // Overloads for primitives // // No overloads are available for signed and unsigned char because if people // are explicitly declaring their chars as signed or unsigned then they are // probably using them as 8-bit integers and would probably prefer an integer // representation. However, we can't really know, so we make the caller decide // what to do. Arg(char value)// NOLINT(runtime/explicit) : piece_(scratch_, 1) { scratch_[0] = value; } Arg(short value)// NOLINT(*) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(unsigned short value)// NOLINT(*) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(int value)// NOLINT(runtime/explicit) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(unsigned int value)// NOLINT(runtime/explicit) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(long value)// NOLINT(*) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(unsigned long value)// NOLINT(*) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(long long value)// NOLINT(*) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(unsigned long long value)// NOLINT(*) : piece_(scratch_, numbers_internal::FastIntToBuffer(value, scratch_) - scratch_) {} Arg(float value)// NOLINT(runtime/explicit) : piece_(scratch_, numbers_internal::SixDigitsToBuffer(value, scratch_)) { } Arg(double value)// NOLINT(runtime/explicit) : piece_(scratch_, numbers_internal::SixDigitsToBuffer(value, scratch_)) { } Arg(bool value)// NOLINT(runtime/explicit) : piece_(value ? "true" : "false") {} Arg(Hex hex);// NOLINT(runtime/explicit) Arg(Dec dec);// NOLINT(runtime/explicit) // `void*` values, with the exception of `char*`, are printed as // "0x<hex value>". However, in the case of `nullptr`, "NULL" is printed. Arg(const void* value);// NOLINT(runtime/explicit) Arg(const Arg&) = delete; Arg& operator=(const Arg&) = delete; absl::string_view piece() const { return piece_; } private: absl::string_view piece_; char scratch_[numbers_internal::kFastToBufferSize]; };
對於double
和float
則使用SixDigitsToBuffer
將浮點轉變為%g
的格式。如果要實現固定長度輸出,則可以使用absl::Hex
absl::Dec
。
absl::Substitute("$0$1$2$3$4 $5", // absl::Dec(0), absl::Dec(1, absl::kSpacePad2), absl::Dec(0xf, absl::kSpacePad2), absl::Dec(int16_t{-1}, absl::kSpacePad5), absl::Dec(int16_t{-1}, absl::kZeroPad5), absl::Dec(0x123456789abcdef, absl::kZeroPad16));
對於 bool 型別,則會輸出true
或者false
。
本質上來說Substitute
是一種簡化的格式化輸出方案,使用編譯器過載解決了執行時檢查型別的麻煩,因此,這種方案安全程度比較高,效率也非常不錯。但 format 支援的引數個數比較有限。absl::Substitute 同樣實現了編譯器引數個數檢查。
StrCat
在 Abseil 之中還有一個absl::StrCat
用來取代字串格式化。在 C 語言標準庫中,strcat
用於連線字串,引數個數是固定的,並且只支援 C-Style 字串,而 Abseil 團隊利用 C++ 變參模板特性實現了現代的 StrCat.
auto result = absl::StrCat(1, 2, 3, 4, 5, 6, 7, 8, 9, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q"); // 123456789abcdefghijklmnopq
與 Subsititute 一樣,StrCat 也是用了類似Arg
的AlphaNum
將字串和基本型別按照原因(或者 Hex,Dec)拼接成特定的字串,並且在拼接字串時能夠提前 resize 從而減少記憶體分配次數,達到優化的目的,在 GNK 的程式碼中,凡是需要連線字串的操作,我們都使用 StrCat 來操作,避免使用 snprintf 或者 strcat 操作。
在Privexec
,Clangbuilder
和Planck
當中,我借鑑 absl::StrCat 實現了一個寬字元版本的
base::StringCat
不支援 double/float,不支援 Hex,Dec ,僅支援其他基本型別和char*
std::wstring_view
std::wstring
。
非同步訊號安全的字串格式化
上述現代 C++ 格式化方案通常情況下令人滿意,但是當我們需要實現一個非同步訊號安全的格式化輸出方案時,則不得不重新打算,非同步訊號安全指的是在訊號中斷的回撥函式中不得呼叫非非同步安全的函式,由於訊號隨時可能發生,因此,在訊號中斷函式中必須不存在記憶體分配,不能擁有互斥鎖等,在 Glibc 和 musl 之中,由於 snprintf 使用了檔案物件和鎖呼叫了vfprintf
則不是非同步訊號安全的,這很容易理解,由於FILE
使用了快取,需要使用鎖保證執行緒安全。在 OpenBSD 當中,snprintf 實現是非同步訊號安全的,在 Github 上有非同步訊號安全的 snprintf 實現,如c99-snprintf
和safe_snprintf
。前面所說的ngx_snprintf
也可以輕鬆的實現非同步訊號安全。
在 absl::StrFormat 中,檢視原始碼發現absl::SNPrintF
是沒有記憶體分配的,但非同步訊號安全還有待考察。
在 Chromium 專案中,也有一個基於現代 C++ 實現的非同步訊號安全的SafeSNPrintf 。在這個實現中,使用 union 包裝變數,並增加型別資訊,這種常見於 Json, Toml 等格式檔案的解析。在格式化時,解析 format 字串,期望的格式與輸入的引數匹配型別,一旦型別匹配,則正常格式化,不匹配則退出,這種方案比 snprintf 要好的多,畢竟 snprintf 只預期輸入格式正確。
struct Arg { enum Type { INT, UINT, STRING, POINTER }; // Any integer-like value. Arg(signed char c) : type(INT) { integer.i = c; integer.width = sizeof(char); } Arg(unsigned char c) : type(UINT) { integer.i = c; integer.width = sizeof(char); } Arg(signed short j) : type(INT) { integer.i = j; integer.width = sizeof(short); } Arg(unsigned short j) : type(UINT) { integer.i = j; integer.width = sizeof(short); } Arg(signed int j) : type(INT) { integer.i = j; integer.width = sizeof(int); } Arg(unsigned int j) : type(UINT) { integer.i = j; integer.width = sizeof(int); } Arg(signed long j) : type(INT) { integer.i = j; integer.width = sizeof(long); } Arg(unsigned long j) : type(UINT) { integer.i = j; integer.width = sizeof(long); } Arg(signed long long j) : type(INT) { integer.i = j; integer.width = sizeof(long long); } Arg(unsigned long long j) : type(UINT) { integer.i = j; integer.width = sizeof(long long); } // A C-style text string. Arg(const char* s) : str(s), type(STRING) { } Arg(char* s): str(s), type(STRING) { } // Any pointer value that can be cast to a "void*". template<class T> Arg(T* p) : ptr((void*)p), type(POINTER) { } union { // An integer-like value. struct { int64_ti; unsigned char width; } integer; // A C-style text string. const char* str; // A pointer to an arbitrary object. const void* ptr; }; const enum Type type; };
如果要實現std::string
std::string_view
的格式化,我們也可以在 union 中使用如下結構取代const char* str
:
struct { const char *data; size_t len; }stringview;
這樣的好處是std::string/std::string_view
不再需要計算長度。我們還可以使用%v
按照輸入引數的型別自主格式化,這樣就不存在型別不匹配了。
結尾
在格式化函式的過程中,C++ 的不足在於沒有反射,從而無法很好的獲得物件的型別,這樣傳統的格式化方案就容易出現問題,而是用變參模板,在複雜的編碼技巧加成後,使用編譯器的檢查能夠很好的實現型別安全高效的格式化,但是也存在一個問題,那就是程式編譯後提交較大,但都到了 9012,只要不超過 Clang 的體積都是能接受的。