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
372
の 382
を取るためにのみオーバーロードできます 、だから 393
のようなコード コンパイルしない 、間違いを犯すことを不可能にします。
しかし、本当に 400
を書きたい場合はどうでしょうか。 ?演算子の「誤用」の意味を見てみましょう:
419
- 除くすべてを設定420
436
- 除くをすべてクリア448
458
- 除くすべてを切り替えます461
472
- を除くすべてをチェック482
したがって、多くのフラグがあり、1 つ (または少数) を除くすべてのフラグに対して何かを実行したい場合は、概念を入れ替えると便利です。これは合理的であるため、許可する必要があります。より明確に。
関数 491
を簡単に書くことができます これはマスクを取り、適切な組み合わせを返し、506
それは逆です。その後、上記の動作はまだ可能です。必要なのは 519
だけです .
実装
527
全 3 種類 534
、 541
と 551
基本的には同じ実装です。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
です 、 784
と 799
、および 803
、 817
と 826
.それらはすべて 835
です インプレースではなく、単に対応するビット演算に転送します。
このクラスのインターフェース全体が実装の詳細であることに注意してください。
847
と 851
次に、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
と同様 ,1063
と 1079
.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
に対して同一のオーバーロードを使用できます。 、 1253
と 1266
、それぞれが新しい 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 ライブラリの一部であり、ここで見つけることができます。