針對 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 也都是可以編譯過的。 ^^"
