実装の課題 flag_set:タイプ セーフ、誤用しにくいビットマスク

API を作成するとき、関数にさまざまなフラグを渡す必要がある場合があります。フラグは任意に組み合わせることができます。

通常、ビットマスクを使用して実装します:各フラグは整数のビットであり、ビット単位の操作で設定/リセットおよび切り替えが可能です.しかし、単純な実装はあまり良くありません:理由を説明して表示します

ビットマスク

通常、ビットマスクは次のように実装されます:

enum flags
{
 a = 1,
 b = 2,
 c = 4,
 d = 8,
};

int some_flags = a | b;
some_flags &= ~b; // clear b
some_flags |= d; // set c

04 実際のフラグ値を定義するために使用されます。各フラグは 1 ビットで表されるため、列挙子には 2 の累乗が割り当てられます。また、10 を使用してビット演算を直接使用できます。 つまり、ここでビット 1 と 2 が設定された整数は、フラグ 24 です。 36 にフラグを立てます .

ただし、このアプローチには複数の欠点があります。まず、従来の C 49 s はスコープされず、56 に変換されます また、2 つのフラグを組み合わせた後では、69 型のオブジェクトはありません。 もう 76 であるため、型の安全性が失われます。

これらの問題は、C++11 の 87 を使用して修正できます。 .しかし、これにより基になる整数型への変換が妨げられるため、ビットごとの演算子の使用も妨げられます.すべての演算子を個別にオーバーロードする必要があります:

flags operator~(const flags& f)
{
 return flags(~static_cast<int>(f));
}

flags operator|(const flags& a, const flags& b)
{
 return flags(static_cast<int>(a) | static_cast<flags>(b));
}

…

フラグの組み合わせは 92 型のオブジェクトになりました 、および 100 ではありません .欠点は、いくつかのフラグを定義するたびに多くの作業が必要になることです.そして、このアプローチはまだ完全ではありません:

各列挙子に異なる 2 の累乗を手動で与える必要があります。これは面倒な手動作業であり、コピーと貼り付けのエラーを簡単に行うことができます。

しかし、もっと重要なのは、このようなエラーに遭遇したことがありますか?

ビット単位の操作はあまり直感的ではありません。フラグを設定するためのより良い API があれば、このような誤用を防ぐことができるとよいのですが。

それでは、まさにそれを行いましょう。

一般的な考え方

昔ながらの C 119 のように s はあまり安全ではありません。128 を使用したいと考えています 、しかし、その後、演算子をオーバーロードする必要があります。これは手間がかかりすぎるため、132 に対して自動的に生成する必要があります。 フラグとして使用したい

そして、ある種の魔法で演算子を生成するとき、もう少し独創的に考えることができます。141 を返す必要はありません。 複数のフラグの組み合わせを表す何らかの異なる型を返す場合、1 つのフラグのみを受け入れる関数と、フラグとの組み合わせを受け入れる関数を記述できます。間違いを犯した場合、コンパイラは私たちに通知します。

それでは、155 というフラグ コンテナーを用意しましょう。 .このタイプは、どのフラグが設定され、どのフラグが設定されていないかを格納します.160 のように

しかし、偶発的な誤用を防ぐにはどうすればよいでしょうか?

そのためには、一歩下がって全体像を見る必要があります。このスタックオーバーフローの回答が指摘しているように、これらは実行したい操作です:

  • 174 を記述してビットを設定します
  • 185 を書いてビットをクリア/リセットします
  • 195 と書いて少し切り替えます
  • 207 を書いて少しチェック

あなたが気付くのはこれです:リセットは補数演算子を使用する唯一の操作であり、他のすべての操作には補数演算子がありません.これは、2ビットに対してこれを実行したい場合でも当てはまります> そして 220 :

  • 234 を記述して設定
  • 245 の書き込みによるクリア/リセット または 257 (ドモルガンの法則)
  • 268 と書くことでトグル
  • 277 と書いてチェック

複数のあなたをリセットするには 283 ただし、296 と書くとエラーになります。 、これは常に 300 です 2 つの個別の異なるフラグ。

これにより、次の 2 種類の概念を識別できます:フラグ 組み合わせ とフラグマスク フラグの組み合わせは、個別の列挙子または複数の 314 のいずれかです。 フラグの組み合わせを使用して、フラグの設定、切り替え、チェックを行うことができます。フラグ マスクは、フラグの組み合わせを補完するものです。327 それらを一緒に使用して、フラグをクリアします。

それを念頭に置いて、2 つの異なる型 336 を定義できます。 と 344 .Like 350 それらはフラグのコンテナーでもありますが、セマンティック情報を持っています。362 372382 を取るためにのみオーバーロードできます 、だから 393 のようなコード コンパイルしない 、間違いを犯すことを不可能にします。

しかし、本当に 400 を書きたい場合はどうでしょうか。 ?演算子の「誤用」の意味を見てみましょう:

  • 419 - 除くすべてを設定 420
  • 436 - 除くをすべてクリア 448
  • 458 - 除くすべてを切り替えます 461
  • 472 - を除くすべてをチェック 482

したがって、多くのフラグがあり、1 つ (または少数) を除くすべてのフラグに対して何かを実行したい場合は、概念を入れ替えると便利です。これは合理的であるため、許可する必要があります。より明確に。

関数 491 を簡単に書くことができます これはマスクを取り、適切な組み合わせを返し、506 それは逆です。その後、上記の動作はまだ可能です。必要なのは 519 だけです .

実装

527

全 3 種類 534541551 基本的には同じ実装です。3 つすべてが複数のフラグを整数のビットとして格納する必要があります。

したがって、共通のクラスでそれを外部委託することは理にかなっています:

template <typename Enum, typename Tag = void>
class flag_set_impl
{
public:
 using traits = flag_set_traits<Enum>;
 using int_type = typename select_flag_set_int<traits::size()>::type;

 …

private:
 static constexpr int_type mask(const Enum& e)
 {
 return int_type(int_type(1u) << static_cast<std::size_t>(e));
 }

 explicit constexpr flag_set_impl(int_type bits) : bits_(bits)
 {
 }

 int_type bits_;
};

3 つのタイプは共通の動作を共有していますが、3 つの 異なる ことは非常に重要です。 タイプ、565 571 があります パラメータです。これは単なるダミーですが、異なるタイプの 2 つのインスタンス化には 2 つの異なるタイプがあり、オーバーロードなどが可能です。

ビットを整数 581 で保存します 最小の 594 です。 少なくともその数のビットを持つ整数型.実装は特殊化を使用するだけで、あまり興味深いものはありません.

私が防ぎたかった他の問題の 1 つは、値を 606 に割り当てるときに間違いを犯すことです。 フラグ。単にデフォルト値を維持することで防止できます。ただし、対応するマスクを直接指定する代わりに、インデックス マスクは610をシフトすることで簡単に作成できます 623 である適切な回数

static constexpr flag_set_impl all_set()
{
 return flag_set_impl(int_type((int_type(1) << traits::size()) - int_type(1)));
}
static constexpr flag_set_impl none_set()
{
 return flag_set_impl(int_type(0));
}

explicit constexpr flag_set_impl(const Enum& e) : bits_(mask(e))
{
}
template <typename Tag2>
explicit constexpr flag_set_impl(const flag_set_impl<Enum, Tag2>& other)
: bits_(other.bits_)
{
}

2 つの名前付きコンストラクターを追加します。1 つは 630 を返します。 フラグが設定されていない場合と、フラグがすべて設定されている場合があります。2 つ目は、より興味深い点です。整数のすべてのビットを直接使用することはできないため、整数の最大値を直接返すことはできません。上位ビットが 648 s 653 667 と等しくない 、それらの上位ビットは 677 であるため s.それで 682 をシフトします フラグよりも 1 つ多く、699 を減算します .これは機能し、 702 であっても機能します すべてのビットを 719 として使用 オーバーフローは明確に定義されています。

720 である限り、興味深いものではない 2 つの通常のコンストラクターも追加します。 .

constexpr flag_set_impl set(const Enum& e) const
{
 return flag_set_impl(bits_ | mask(e));
}
constexpr flag_set_impl reset(const Enum& e) const
{
 return flag_set_impl(bits_ & ~mask(e));
}
constexpr flag_set_impl toggle(const Enum& e) const
{
 return flag_set_impl(bits_ ^ mask(e));
}

次は、単一のビットを設定/クリア/トグルする重要なメンバー関数です。それらはすべて簡単で、735 を利用します。 740 を取るコンストラクタ .in-place ではなく、新しい 752 を返すことに注意してください。 C++11 761 で動作できるようにする

示されていない他のメンバー関数は 775 です 、 784799 、および 803817826 .それらはすべて 835 です インプレースではなく、単に対応するビット演算に転送します。

このクラスのインターフェース全体が実装の詳細であることに注意してください。

847851

次に、2 つのセマンティック フラグ コンテナーを作成します。

template <typename Enum>
using flag_combo = flag_set_impl<Enum, struct combo_tag>;

template <typename Enum>
using flag_mask = flag_set_impl<Enum, struct mask_tag>;

タグタイプとして、オンザフライの 864 を使用します

ユーザーが今やるべきことは、ビット単位の操作だけです。次のようにオーバーロードします:

  • できます 878 2 つの 885 オブジェクトだけでなく、列挙子とのコンボ、結果は 897 です
  • 902できます 2 つの 911 マスクを生成するオブジェクト
  • 926できます 937 またはマスクを生成する列挙子。
  • 943できます 957 コンボを生み出します。
  • 列挙子を使用したコンボだけでなく、2 つのマスク/コンボの等価性を比較することもできます。

968 と同様に、特定のインターフェースを使用した実装は非常に簡単です。 そして 979

987

993 はユーザーにとって重要なタイプであり、他のタイプについてあまり心配する必要はありません。1005 を使用します。 メンバーとして、すべての関数は単純にそれを転送します。

1015 単純な名前付きメンバー関数を提供します:1025 ,1036 ,1040 1052 と同様 ,10631079 .1081 とは異なります ユーザーと 1091 にとってより便利であるため、それらはインプレースで動作します 1104 もあります オーバーロード。

フラグの組み合わせから作成することもできます (例:1115 または列挙子) に割り当てられます:

template <typename FlagCombo, typename = detail::enable_flag_combo<FlagCombo, Enum>>
constexpr flag_set(const FlagCombo& combo) noexcept : flags_(combo)
{
}

1126 1134 の便利な別名です 、および 1149 です:

template <typename T, typename Enum>
struct is_flag_combo : std::false_type
{
};

template <typename Enum>
struct is_flag_combo<Enum, Enum> : flag_set_traits<Enum>
{
};

template <typename Enum>
struct is_flag_combo<flag_combo<Enum>, Enum> : flag_set_traits<Enum>
{
};

トレイトに戻ります。それ以外の場合は、引数が 1152 かどうかをチェックするだけです。 直接または 1160 .非常に単純な SFINAE により、変換が 1177 に対してのみ機能することが保証されます 1184 ではありません .

1194 複合ビット演算 1208 も提供します と 1214 コンストラクタのように制約されます 1225 1237 が必要です 、思い通りに潜在的な間違いを見つけました。

もう少し興味深いのは、非複合演算子です。1247 に対して同一のオーバーロードを使用できます。 、 12531266 、それぞれが新しい 1271 を返します 1281 を使用しています。 ビットが設定されているかどうかを確認します。この 1296 マスクではなくフラグの組み合わせを受け取り、1308 も返す必要があります .

しかし、これをフラグの組み合わせとして追加するのは簡単で、フラグ マスクは 2 つの異なるタイプです。他の実装とは異なり、1310 への変換を取り除くことができます。 1325

1339 のオーバーロードを自動的に生成する

最後のピースが 1 つ欠けていることを除いて、すべてを実行しました:1345 のビット演算はまだありません 直接、オーバーロードできるのは、少なくとも 1 つのユーザー定義型を取るものだけです。

1350 1363 に含まれるフラグの数も知る必要があります。 、整数型を選択して 1378 を実装するために コンストラクター。

1383 を導入することで、2 つの問題を同時に解決できます。 .これは、独自のタイプに特化できるクラス テンプレートです。 1396 s. 1404 を提供する必要があります 関数 1418 1425 のフラグの数を返します 1439 が使用 .

また、ビット演算を「生成」するためにも使用できます。 1446 の型がわからないため、それらを直接オーバーロードすることはできません。 まだ.だから私たちができることは、グローバルスコープでテンプレートとしてそれらを書くことだけです.

しかし、すべて タイプは突然 1458 になります 、彼らが実際に提供するものよりも良い一致かもしれません!

これは明らかに悪い考えなので、代わりにテンプレートを制約することができます。型が 1469 の場合にのみ、SFINAE を使用してテンプレートを有効にできます。 特化した 1478 で 特殊化を検出することも難しくなく、すべての特殊化が 1481 から継承することを単純に要求できます。 1494 をチェックしてください .

現在、これはまだ良い解決策ではありません - それはまだグローバルなテンプレート化されたオペレーターですが、良い解決策はありません.「手動で行う」以外の唯一の解決策は、マクロを使用することです.

この手法を使用して、不足している演算子を追加できます。

template <typename Enum, typename = type_safe::detail::enable_flag<Enum>>
constexpr type_safe::flag_mask<Enum> operator~(const Enum& e) noexcept
{
 return type_safe::flag_mask<Enum>::all_set().reset(e);
}

template <typename Enum, typename = type_safe::detail::enable_flag<Enum>>
constexpr type_safe::flag_combo<Enum> operator|(const Enum& a, const Enum& b) noexcept
{
 return type_safe::flag_combo<Enum>(a) | b;
}

フラグの補数を構築するときはマスクを作成する必要があり、1 つまたは 2 つ一緒に使用するときは組み合わせを作成する必要があります。

正しい 1507 を自動的に使用

1518 によるアプローチ ただし、少し醜いです:1528 を定義するとき 名前空間を閉じて、1530 の名前空間を開く必要があります 、それを特殊化し、他に何かを追加する必要がある場合は、元のものをもう一度開きます。

デフォルトの 1545 の方が良いでしょう 特殊化はそれ自体で機能します。これは、邪魔になるという代償を払って行うこともできます。デフォルトの 1554 引数が 1569 かどうかを確認できます 特別な列挙子 (1570 など) があるかどうか .その場合は 1586 から継承します 1597 を使用 1601 の戻り値として 、それ以外の場合は 1613 から継承します .

結論

次のコードを書くだけでフラグを実装する方法を作成しました:

enum class flags
{
 a,
 b,
 c,
 …
 _flag_set_size
};

2 の累乗を割り当てる必要はなく、マクロやオーバーロード演算子を使用する必要もありません。そのまま使用できます。

さらに、型システムを使用してビット単位の操作にセマンティック情報を与えるため、コンパイラは演算子を誤用したときによくある間違いをチェックできます。タイプの使用は隠されています。

完全な実装は、私の type_safe ライブラリの一部であり、ここで見つけることができます。