C++11/14 literals:part 2

| | 0 Comments| 15:06
Categories:

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

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *