針對 C++11/C++14 的 literals,前面已經寫了 part 1 來針對標準提供的 literals 做了一些說明;而接下來這篇,則是來紀錄一下如何自訂屬於自己的 literals、也就是所謂「User-defined literals」(CppReference)。
在定義 User-defined literals 的時候,能支援的格式是有限制的,包括了:
- 整數:(unsigned long long int)
- 浮點數:(long double)
- 字元:(char)、(wchar_t)、(char16_t)、(char32_t)
- 字串:(char,size_t)、(wchar_t,size_t)、(char16_t,size_t)、(char32_t,size_t)
如果不是這些型別的話,是不能編譯的。
<!–more–>
實作上,在前一篇已經有透過角度弧度轉換來做為範例了。
基本上,同類型的應用也還滿多的,像是重量、長度等等的單位轉換,基本上都可以透過這樣的概念來完成。
不過,個人覺得他比較有價值的地方,應該還是用來協助建立物件。
比如說這邊有一個自定義的 mytype,那也可以透過定義 _mytype 這樣的 literal 來建立物件。
struct mytype { unsigned long long m; }; constexpr mytype operator"" _mytype(unsigned long long n) { return mytype{ n }; }
使用時就是:
mytype x = 123_mytype;
在這個例子裡面,這樣做看來並沒什麼意義。不過如果考慮到類似 std::chrono 的時間用的 literals 這樣、有許多種單位的情況下,針對不同的單位來自定 literals 取代複雜的建構流程,是會有幫助、而且更直覺的。
這邊是試著寫一個距離的類別:
class CDistance { public: enum class TUnit : unsigned int { mm = 1, cm = 10, m = 1000, }; CDistance(const unsigned long long uVal,const TUnit eUnit) { m_val = uVal; m_unit = eUnit; } public: unsigned long long m_val = 0; TUnit m_unit = TUnit::cm; };
這樣正常要定義一個 100cm 出來,應該會是寫成:
auto a = CDistance(100, CDistance::TUnit::cm);
而如果要簡化的話,則可以針對 mm、cm、m 各自定義 literals 的話,則可以寫成:
CDistance operator"" _mm(unsigned long long v) { return CDistance{ v, CDistance::TUnit::mm }; } CDistance operator"" _cm(unsigned long long v) { return CDistance{ v, CDistance::TUnit::cm }; } CDistance operator"" _m(unsigned long long v) { return CDistance{ v, CDistance::TUnit::m }; }
這樣要定義一個 100cm 出來,就可以簡化成:
auto b = 100_cm;
當要大量使用的時候,這樣是會簡單很多的~
類似的東西,在微軟網站上也有類似的範例(參考)。
在自定義 literal 的時候,有幾點是可能要注意的:
-
雖然目前的編譯器似乎都還接受,但是原則上應該是要以「_」(underscore)開頭,這個被稱為「ud-suffix」。
以 MSVC 和 g++ 來說,現在應該都會給對應的警告。 -
如果是以「_」然後接一個大寫字元的話,會變成是保留字(例如「operator "" _Z」);要使用的話跟前面的 "" 之間不能有空格(例如「operator ""_Z」)。
不過目前 MSVC 和 g++ 似乎都不會出錯?
另外,針對整數和浮點數的 User-defined literals 來說,除了使用 unsigned long long int 和 long double 來做為輸入的參數外,也可以使用以 const char* 作為輸入參數的「raw literal operator」來做處理。(raw literal operator 的優先度較低)
像之前角度弧度轉換的例子來說,當時所定義的 literals 只支援浮點數,如果給整數(例如 180_deg)是會編譯錯誤的;而如果改定義成「raw literal operator」的話,則會變成整數、浮點數都可以使用:
long double operator"" _deg(const char* str) { return std::atof(str) * 3.14159265358979323846264L / 180; } int main(int argc, char** argv) { auto v1 = 180_deg; auto v2 = 180.0_deg; return 0; }
但是這樣做的缺點,就是這個 User-defined literal 因為要把字串轉換成數字,所以無法在編譯階段處理、而是要在執行階段才能運算,效能會差一點就是了。
此外,其實也還有更符雜的「literal operator template」可以用;他的基本形式大概是像下面的樣子:
template <char...> double operator "" _x() { }
不過 Heresy 自己是沒想到什麼東西可以/需要這樣玩就是了…
(這裡有找到兩個例子)
另外,在使用的時候,在某些情況下,如果後面直接接著 +、- 或是 . 的話,可能會導致編譯器無法正確編譯,這時候需要使用括號或加上空白;下面是一些例子:
long double operator""_E(long double); long double operator""_a(long double); int operator""_p(unsigned long long); auto x = 1.0_E+2.0; // error auto y = 1.0_a+2.0; // OK auto z = 1.0_E +2.0; // OK auto q = (1.0_E)+2.0; // OK auto w = 1_p+2; // error auto u = 1_p +2; // OK #include <chrono> using namespace std::literals; auto a1 = 4s.count(); // Error auto b2 = 4s .count(); // OK auto c3 = (4s).count(); // OK
不過,這邊本來一些會出錯的狀況,在 MSVC 2019 也都是可以編譯過的。 ^^"