enum class 的 bitwise operation

| | 0 Comments| 10:02
Categories:

這篇算是之前《使用 enum class 取代傳統的 enum》的後續。

當時介紹的 enum class 基本上是 C++11 引入、強化列舉型別的型別強度的設計;透過這樣的設計,可以避免一些程式設計上可能會碰到的問題。

但是,當 Heresy 後來實際要使用的時候,才發現 enum class 在某些方面上,似乎還有一些地方沒辦法和 enum 一樣地使用。

其中一個比較大的困擾,就是 enum class 沒有預設提供 bitwise operation,所以沒辦法直接把 enum class 當成 bitmask 來用。

比如說,以往以前可能會有這樣的寫法:

enum EFlag
{
  FlagB0 = 0b00000000,
  FlagB1 = 0b00000001,
  FlagB2 = 0b00000010,
  FlagB12 = FlagB1 | FlagB2
};
int main(int argc, char** argv)
{
  if ((EFlag::FlagB12 & EFlag::FlagB1) == EFlag::FlagB1)
  {
    std::cout << "OK" << std::endl;
  }
  return 0;
}

這樣的寫法是沒有問題的。

但是如果改用 enum class 寫成:

enum class EFlag : uint8_t
{
  FlagB0 = 0b00000000,
  FlagB1 = 0b00000001,
  FlagB2 = 0b00000010,
  FlagB12 = FlagB1 | FlagB2
};

的話,那

if ((EFlag::FlagB12 & EFlag::FlagB1) == EFlag::FlagB1)

這一行,就會因為沒有合適的 operator & 而編譯失敗。

使用一般的 enum 的時候,之所以可以運作,主要是因為 enum 可以自動轉型成 int 來進行 bitwise 的計算,所以不會有問題;但是由於 enum class 不允許這樣的自動轉型,所以就沒辦法運作了。


基本解法

而解決方法呢?基本上就是需要自己針對自己定義的 enum class 去實作對應的 operator &、以及其他需要的 bitwise operator  了~(一般主要是「&|^~」這四個,如果有需要,可能還有 &=、|=、^=)

operator & 來說,內容大概就會是:

EFlag operator&(EFlag left, EFlag right)
{
  return static_cast<EFlag>(
    static_cast<uint8_t>(left) &
static_cast<uint8_t>(right) ); }

可以看到,這邊很簡單,就是先把 EFlag 強制轉型成底層的型別(uint8_t),進行計算完後,再轉型回 EFlag 並回傳。

而如果擔心以後 EFlag 底層型別可能會修改,怕到時候出現不一致的狀況的話,其實也可以透過 C++11 <type traits> 所提供的 std::underlying_type參考)來在編譯階段取得底層的型別。

其寫法會變成:

EFlag operator&(EFlag left, EFlag right)
{
  return static_cast<EFlag>(
    static_cast<std::underlying_type_t<EFlag>>(left) &
    static_cast<std::underlying_type_t<EFlag>>(right) );
}

這樣就算以後修改 EFlag 的底層型別,也不必修改這些 bitwise operator 的內容了~


通用的版本

如果有定義的 enum class 很多,又都需要用到 bitwise operation 的話,那其實可以考慮用 template 來實作這些 operator。

例如:

template<typename TEnumType>
TEnumType operator&(TEnumType left, TEnumType right)
{
  return static_cast<TEnumType>(
    static_cast<std::underlying_type_t<TEnumType>>(left) &
    static_cast<std::underlying_type_t<TEnumType>>(right) );
}

但是這樣的缺點,是所有的型別都可以套用到這些 bitwise operator,雖然因為有用到 std::underlying_type,所以其實也不一定可以編譯過就是了。

不過實際上,這樣應該還不是一個好的方法,可以的話最好還是要確認是我們要的列舉型別才支援。

如果要這樣實作的話,基本上可以透過 std::enable_if參考,一樣是 <type traits> 的東西)來做控制。

例如:

template<typename TEnumType>
struct support_bitwise_enum : std::false_type {};
template<typename TEnumType>
typename std::enable_if_t<support_bitwise_enum<TEnumType>::value, TEnumType>
operator&(TEnumType left, TEnumType right)
{
  return static_cast<TEnumType>(
    static_cast<std::underlying_type_t<TEnumType>>(left) &
    static_cast<std::underlying_type_t<TEnumType>>(right));
}

這樣的話,如果要讓自己的 enum class 支援 bitwise operation,就只需要另外定義一個

template<>
struct support_bitwise_enum<EFlag> : std::true_type {};

讓他繼承 std::true_type 就可以了~(這邊是參考 std::error_code 的用法)

而如果沒有做這個顯示特定化(explicit specialization)的話,基本上就不能使用這個 operator & 了。

而透過這樣的方法,把所有的 bitwise operator 都實作一遍、包成一個 header 的話,就可以很方便地來使用,而且也可以控制要讓那些 enum class 用了。

例如,假設包成一個這樣的 bitwise_enum.hppGitHub),那之後要使用的時候,就可以像下面的程式:

#include <iostream>
#include "bitwise_enum.hpp"
enum class EFlagN
{
  Flag0 = 0,
  Flag1 = 1,
  Flag2 = 2,
  Flag12 = 3
};
enum class EFlag
{
  FlagB0 = 0b00000000,
  FlagB1 = 0b00000001,
  FlagB2 = 0b00000010,
  FlagB12 = FlagB1 | FlagB2
};
template<>
struct support_bitwise_enum<EFlag> : std::true_type {};
int main(int argc, char** argv)
{
  if ((EFlagN::Flag12 & EFlagN::Flag2) == EFlagN::Flag2) // Error!
  {
    std::cout << "EFlagN OK" << std::endl;
  }
  if ((EFlag::FlagB12 & EFlag::FlagB2) == EFlag::FlagB2)
  {
    std::cout << "EFlag OK" << std::endl;
  }
  return 0;
}

其中 EFlagN 就會因為沒有透過定義 support_bitwise_enum<>,所以無法使用 bitwise operator 而編譯失敗;而 EFlag 因為有將 support_bitwise_enum<EFlag> 設定成 true_type,所以就可以正確編譯、使用。

而如果是希望所有 enum class 都可以適用的話,其實也可以考慮改用 std::is_enum參考)來作為判斷條件,但是相對地,就會失去可控制的可能性了。


所以,理論上只要有 bitwise_enum.hpp 這個包裝好的 header 檔,以後要定義 enum class 的時候,就可以很簡單地讓他支援 bitwise operation 了。

不過,老實說說,Heresy 總覺得…為什麼這種功能沒有加到標準函式庫裡啊?
(還是其實有,只是 Heresy 不知道?)


參考:《Using enum classes as type-safe bitmasks

Leave a Reply

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