摘要訊息 : C++ 11 中關於字串的兩個新特性.
0. 前言
除了和多執行緒相關的內容之外, Jonny'Blog 已經幾乎將 C++ 11 的內容全部囊括其中. 但是用戶自訂字面值一直是我們沒有講述的內容. 這是因為這個特性我實在是找不到使用的理由, 它的限制太嚴格的了. 直到 C++ 20 為止, 它的限制幾乎沒有什麼大的改變, 這彷彿是專門為 C++ 標準樣板程式庫專門提出的特性. 事實上除了標準樣板程式庫之外, 我也很少在程式庫或者程式設計中看到有人使用這樣的特性, 它甚至比 C++ 11 引入的屬性還要冷門. 另一個內容是原始字串, 這個特性也幾乎沒什麼人使用. 我認為這主要是因為 C++ 中的原始字串比較複雜的原因.
更新紀錄 :
- 2022 年 6 月 16 日進行第一次更新和修正.
1. 用戶自訂字面值
對於 1ull
, 2.0lf
和 u16"123"
類似的字面值來說, 前綴和後綴都代表著這個字面值應該是什麼樣的型別. 但是對於非內建型別, 比如複數型別和分數型別這樣的自訂型別, 我們沒有辦法使用類似的前綴或者後綴來描述它們, 只能通過對應的建構子來建構. 因此, C++ 11 就引入了用戶自訂字面值, 它允許我們使用 12_km
表示 12 千米, 使用 8_h
表示 8 小時等. 實質上, 用戶自訂字面值是一個多載函式, 它具有如下形式 : R operator"" user-defined-suffix(parameter-list);
. 其中, R
表示回傳型別; user-defined-suffix
是用戶自訂後綴, 之前所說的 _km
和 _h
就是我們自訂的後綴, 不能以數字開頭, 不能存在特殊字元; parameter-list
是這個函式的參數列表. 這個宣告是一個函式宣告, 因此任何可以用於函式上的特性都可以用於這個宣告, 例如 constexpr
, inline
和 noexcept
標識, 還有屬性等.
支援用戶自訂的字面值有 :
- 二進制字面值;
- 八進制字面值;
- 十進制字面值;
- 十六進制字面值;
- 字元字面值;
- 字串字面值.
其中, 數字字面值指的是前面四個, 它可以是整型的, 也可以是浮點數的, 可以被分隔符 '
隔開 (分隔符在處理的時候會被忽略, 參考《【C++ 14】使用單引號作為長數字的分隔》), 甚至可以是科學計數的.
現在我們寫一個關於用戶自訂字面值的最簡單的程式 :
constexpr double operator"" _to_double(unsigned long long) noexcept {
return 1.0;
}
int main(int argc, char *argv[]) {
double a {123_to_double}; // OK
int b {123_to_double}; // Error : type 'double' cannot be narrowed to 'int' in initializer list
}
在 Code 1 中, 我們把任何帶有 _to_double
後綴的數字統一轉換成了 double
型別的 1.0
字面值. 那麼如果將上面的後綴改為類似於 LL
和 ull
這種本身就是內建的後綴是否可行呢? 答案是可行的, 不產生任何編碼錯誤 (但是部分編碼器可能會對這個行為擲出編碼警告), 因為我們可以通過自訂來讓字串和字元也可以支援 LL
和 ull
這種後綴. 但是對於字面值數字, 編碼器永遠不會運作我們自訂的那個函式, 因為內建的後綴永遠優先於用戶自訂的字面值後綴.
對於用戶自訂字面值函式的參數列表, 我們有很嚴格的限定.
非樣板的用戶自訂數字字面值函式的參數列表有且唯有一個參數, 且型別為 unsigned long long
或者 long double
, 不可以使用其它任何型別. 因為 unsigned long long
和 long double
分別是最大的內建整型型別和浮點數型別. 對於負的整數來說, 例如在之前程式碼中的 -123_to_double
, 編碼器首先會針對 123_to_double
進行轉換, 然後將負號放置在轉換之後的結果上, 最終的結果為 -1
. 對於樣板化的用戶自訂字面值函式, 它不能接受任何參數, 且樣板只能宣告為 template <char ...>
或者 template <typename T, T ...>
. 對於 template <char ...>
, 任何字面值都會被編碼器拆解為單個字面值, 然後作為樣板引數. 例如對於
#include <iostream>
template <char ...Cs>
constexpr double operator"" _to_double() noexcept {
std::cout << sizeof...(Cs) << std::endl;
return 1.0;
}
如果我們使用了 123456_to_double
, 那麼編碼器實際上是呼叫了 operator"" _to_double<'1', '2', '3', '4', '5', '6'>()
. 上述程式碼中的 sizeof...(Cs)
的值為 6
. 對於浮點數, 小數點也會被當成獨立的字元放入樣板引數中, 例如 12.3456_to_double
, 上述程式碼中的 sizeof...(Cs)
的值為 7
.
字元字面值的自訂, 其參數列表也只能存在一個參數, 不過型別可以從 char
, wchar_t
, char16_t
, char32_t
和 char8_t
(since C++ 20) 中選擇一個. 對於 char
型別, 不可以為其標記 signed char
或者 unsigned cha
r, 字元字面值的用戶自訂函式的參數列表只接受 char
. 剩餘的用法和用戶自訂的數字字面值是類似的, 此處不再累贅.
字串字面值也可以自訂, 它的參數列表可以有一個或者兩個參數 : 第一個參數必須是字面值字串的型別, 可以從 const char *
, const wchar_t *
, const char16_t *
, const char32_t *
和 const char8_t *
(since C++ 20) 中進行選擇; 第二個參數是可選的, 它被限定為 std::size_t
型別, 用於描述字面值字串不包含終結字元的字串長度.
用戶自訂的字串字面值也可以進行拼接 :
#include <iostream>
using namespace std;
constexpr int operator"" _to_int(const char *, size_t) {
return 1;
}
constexpr int operator"" _to_int(const char16_t *, size_t) {
return 0;
}
constexpr int operator"" _to_int(const char32_t *, size_t) {
return 2;
}
constexpr int operator"" _to_int(const wchar_t *, size_t) {
return 3;
}
int main(int argc, char *argv[]) {
cout << "123" "vvvv" "000"_to_int << endl; // 輸出結果 : 1
cout << "123"_to_int "vvv"_to_int << endl; // 輸出結果 : 1
cout << u"123"_to_int ""_to_int << endl; // 輸出結果 : 0
cout << "123"_to_int L""_to_int << endl; // 輸出結果 : 3
cout << u"123"_to_int L""_to_int << endl; // Error : unsupported non-standard concatenation of string literals
cout << U"123"_to_int L""_to_int << endl; // Error : unsupported non-standard concatenation of string literals
}
由此, operator""
成為了一個字面值運算子, 而不僅僅是代表著一個字串.
從上面來看, 大家可能覺得限制僅僅是在運算子多載中的參數罷了. 其它的運算子多載對於參數數量也有限制, 只不過用戶自訂的字面值多載的函式對參數的型別有更多的限制. 但是實際上, 限制還有更多. 大家可以看到, 我自訂的後綴都是以 _
打頭的. C++ 規定了所有不以 _
打頭的後綴都被保留, 大家見到不以 _
打頭的後綴大多是在使用 C++ 標準樣板程式庫的時候. 大家使用用戶自訂字面值的時候, 就是為了方便, 以 _
打頭不但不好看, 也不方便, 這很不 C++. 還有, 所有以 _
打頭並且首個字元為大寫的用戶自訂後綴也是被保留的.
對於字串的用戶自訂字面值, 上面說到第二個參數 std::size_t
是可選的. 如果在對於字串多載的字面值運算子中省略掉第二個參數 std::size_t
並不會產生編碼錯誤, 但是實際使用的時候又是另外一回事 :
#include <iostream>
constexpr int operator"" _to_int(const char *) {
return 1;
}
int main(int argc, char *argv[]) {
std::cout << ""_to_int << std::endl; // Error : no matching literal operator for call to 'operator""_to_int' with arguments of types 'const char *' and 'unsigned long', and no matching literal operator template
}
當然, 運算子多載不支援存在預設引數, 字面值運算子也是如此. 但是一般的運算子多載支援使用連結指示, 但是多載的字面值運算子不支援 extern "C"
:
enum E {};
extern "C" {
bool operator+(int a, E b) {
return false;
}
bool operator"" _to_bool(const char *) { // Error : literal operator must have C++ linkage
return true;
}
}
bool operator"" _to_bool(const char *) { // OK
return true;
}
現在來了解一個更特殊的情形 :
struct S {
void func() {}
};
constexpr S operator"" _S(unsigned long long) noexcept {
return {};
}
int main(int argc, char *argv[]) {
12_S.func(); // Error : no matching literal operator for call to 'operator""_S.func' with argument of type 'unsigned long long' or 'const char *', and no matching literal operator template
}
我們的本意是通過用戶自訂字面值將 12
轉換為 S
型別的預設值之後, 呼叫類別 S
中的成員函式 func
, 然而產生了編碼錯誤. 因此, 我們要在中間增加一個空格 12_S .func()
; 或者直接加入括號 (12_S).func()
才行. 通過這個實例, 我們也很顯然可以知道, 使用科學計數法之後 (以 e
和 E
結尾的數字), 也會有類似的問題 :
#include <iostream>
using namespace std;
struct S {
void func() {}
int operator-(int) {
return 0;
}
};
constexpr S operator"" _e(unsigned long long) noexcept {
return {};
}
int main(int argc, char *argv[]) {
cout << fixed;
cout << 5e-3 << endl; // 輸出結果 : 0.005000
cout << 5_e-3 << endl; // Error : no matching literal operator for call to 'operator""_e-8' with argument of type 'unsigned long long' or 'const char *', and no matching literal operator template
cout << 5_e -3 << endl; // 輸出結果 : 0
cout << (5_e)-3 << endl; // 輸出結果 : 0
}
還有一個是關於 std::printf
的. 在引入用戶自訂字面值之後, std::printf("%"PRId64"\n",INT64_MIN);
就會產生編碼錯誤, 需要增加一個空格, 修改為 std::printf("%" PRId64"\n",INT64_MIN);
才可以.
上面這麼多限制, 我是真的想不到有什麼使用它的理由, 儘管它看起來很簡潔, 但這是針對 C++ 標準樣板程式庫而言的.
最後, C++ 14, C++ 17 和 C++ 20 在標準樣板程式庫中使用了用戶自訂字面值 :
if
,i
和il
後綴分別表示std::complex<float>
,std::complex<double>
和std::complex<long double>
的字面值 (since C++ 14);h
,min
,s
,ms
,us
和ns
後綴分別表示std::chrono::duration
中小時, 分鐘, 秒, 毫秒, 微秒和納秒的字面值 (since C++ 14);s
後綴表示std::string
的字面值 (since C++ 14);sv
後綴表示std::string_view
的字面值 (since C++ 17);y
和d
後綴表示std::chrono::year
和std::chrono::day
的字面值 (since C++ 20).
2. 原始字串
如果某個字串中包含了較多需要轉譯的字元, 那麼就可以使用 C++ 11 引入的原始字串, 它的基本形式為 R"()"
. 其中, 前綴 R
表示了這個字串是一個原始字串, 字串中的括號不能省略, 括號之內的字串是真正的字串. 例如字串 "\\\n\'\"\\\\"
使用原始字串就可以直接寫為 R"(\
. 記著, 這裡有一個換行. 也就是說, 在一個原始字串中, 如果換一行繼續寫, 那麼確實代表了其中有一個換行字元 :
'"\\)"
#include <iostream>
int main(int argc, char *argv[]) {
std::cout << (string("\\\n\'\"\\\\") == string(R"(\
'"\\)")) << std::endl; // 輸出 : 1
std::cout << (string("\\\n\'\"\\\\") == string(R"(\\n'"\\)")) << std::endl; // 輸出 : 0, \n 在原始字串中被解釋為 '\\' 和 'n' 兩個字元
}
本來字元 )
和 "
是不支援在原始字串中出現的, 如果要出現, 就需要使用分隔符 : R"delimiter()delimiter"
. 其中, delimiter
就是分隔符. 分隔符可以是任何不包含 "\
", 空格, "(
" 和 ")
" 的字串, 長度不能超過 16 個字元. 因此, 任何滿足上述條件的字串都可以作為分隔符, 不一定必須寫為 delimiter
. 例如, R"asd()asd
" 和 R"dva++,weq()dva++,weq"
的分割符為 asd
和 dva++,weq
. 並且,
#include <iostream>
int main(int argc, char *argv[]) {
std::string a {R"asd()asd"};
std::string b {R"dva++,weq()dva++,weq"};
std::cout << (a == b) << std::endl; // 輸出結果 : 1
}
但是通過 Code 7, 我發現並不需要分隔符, 很可能是現代編碼器會自動判定字面值原始字串到底哪一個 "
和 )
是作為字串結束的標誌. 因此, 對於現代編碼器來說, )
和 "
這兩個字元可以直接出現在原始字串中.
在預設情況下, R"()"
表示型別為 const char []
的字面值字串. 若要使用其它型別的字元, 可以使用下列前綴 :
LR"()"
表示const wchar_t []
型別的原始字串;uR"()"
表示const char16_t []
型別的原始字串;UR"()"
表示const char32_t []
型別的原始字串;u8"()"
表示const char8_t []
型別的原始字串 (since C++ 20).
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :