web-dev-qa-db-ja.com

C ++列挙型フラグとビットセット

列挙型フラグの使用ビットセットの長所と短所は何ですか?

namespace Flag {
    enum State {
        Read   = 1 << 0,
        Write  = 1 << 1,
        Binary = 1 << 2,
    };
}

namespace Plain {
    enum State {
        Read,
        Write,
        Binary,
        Count
    };
}

int main()
{
    {
        unsigned int state = Flag::Read | Flag::Binary;
        std::cout << state << std::endl;

        state |= Flag::Write;
        state &= ~(Flag::Read | Flag::Binary);
        std::cout << state << std::endl;
    } {
        std::bitset<Plain::Count> state;
        state.set(Plain::Read);
        state.set(Plain::Binary);
        std::cout << state.to_ulong() << std::endl;

        state.flip();
        std::cout << state.to_ulong() << std::endl;
    }

    return 0;
}

これまで見てきたように、ビットセットは処理するためのより便利なset/clear/flip関数を持っていますが、enum-flagsの使用はより広く普及したアプローチです。

ビットセットの欠点は何ですか?また、毎日のコードで何をいつ使用する必要がありますか?

15

最適化を有効にしてコンパイルしますか? 24倍速になることはほとんどありません。

私にとって、ビットセットはあなたのためにスペースを管理するので優れています:

  • 必要なだけ拡張できます。フラグが多い場合、int/long longバージョンでスペースが不足する可能性があります。
  • いくつかのフラグのみを使用する場合、必要なスペースが少なくなる可能性があります(unsigned char/unsigned shortに収まる可能性があります-実装がこの最適化を適用するかどうかはわかりません)
2
geza

_std::bitset_とc-style enumには、フラグを管理する上で重要な欠点があります。まず、次のコード例を考えてみましょう。

_namespace Flag {
    enum State {
        Read   = 1 << 0,
        Write  = 1 << 1,
        Binary = 1 << 2,
    };
}

namespace Plain {
    enum State {
        Read,
        Write,
        Binary,
        Count
    };
}

void f(int);
void g(int);
void g(Flag::State);
void h(std::bitset<sizeof(Flag::State)>);

namespace system1 {
    Flag::State getFlags();
}
namespace system2 {
    Plain::State getFlags();
}

int main()
{
    f(Flag::Read);  // Flag::Read is implicitly converted to `int`, losing type safety
    f(Plain::Read); // Plain::Read is also implicitly converted to `int`

    auto state = Flag::Read | Flag::Write; // type is not `Flag::State` as one could expect, it is `int` instead
    g(state); // This function calls the `int` overload rather than the `Flag::State` overload

    auto system1State = system1::getFlags();
    auto system2State = system2::getFlags();
    if (system1State == system2State) {} // Compiles properly, but semantics are broken, `Flag::State`

    std::bitset<sizeof(Flag::State)> flagSet; // Notice that the type of bitset only indicates the amount of bits, there's no type safety here either
    std::bitset<sizeof(Plain::State)> plainSet;
    // f(flagSet); bitset doesn't implicitly convert to `int`, so this wouldn't compile which is slightly better than c-style `enum`

    flagSet.set(Flag::Read);    // No type safety, which means that bitset
    flagSet.reset(Plain::Read); // is willing to accept values from any enumeration

    h(flagSet);  // Both kinds of sets can be
    h(plainSet); // passed to the same function
}
_

これらの問題は簡単な例で簡単に見つけられると思うかもしれませんが、Cスタイルのenumと_std::bitset_の上にフラグを構築するすべてのコードベースに忍び寄ります。

では、型の安全性を高めるために何ができるでしょうか?まず、C++ 11のスコープ付き列挙型は、型の安全性の向上です。しかし、それは多くの利便性を妨げます。ソリューションの一部は、スコープ付き列挙型にテンプレートで生成されたビットごとの演算子を使用することです。これがどのように機能するかを説明し、また動作するコードを提供する素晴らしいブログ投稿です: https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html

これがどのようになるか見てみましょう:

_enum class FlagState {
    Read   = 1 << 0,
    Write  = 1 << 1,
    Binary = 1 << 2,
};
template<>
struct enable_bitmask_operators<FlagState>{
    static const bool enable=true;
};

enum class PlainState {
    Read,
    Write,
    Binary,
    Count
};

void f(int);
void g(int);
void g(FlagState);
FlagState h();

namespace system1 {
    FlagState getFlags();
}
namespace system2 {
    PlainState getFlags();
}

int main()
{
    f(FlagState::Read);  // Compile error, FlagState is not an `int`
    f(PlainState::Read); // Compile error, PlainState is not an `int`

    auto state = Flag::Read | Flag::Write; // type is `FlagState` as one could expect
    g(state); // This function calls the `FlagState` overload

    auto system1State = system1::getFlags();
    auto system2State = system2::getFlags();
    if (system1State == system2State) {} // Compile error, there is no `operator==(FlagState, PlainState)`

    auto someFlag = h();
    if (someFlag == FlagState::Read) {} // This compiles fine, but this is another type of recurring bug
}
_

この例の最後の行は、コンパイル時にまだキャッチできない1つの問題を示しています。場合によっては、等しいかどうかを比較することが本当に望ましい場合があります。しかし、ほとんどの場合、実際に意味するのはif ((someFlag & FlagState::Read) == FlagState::Read)です。

この問題を解決するには、列挙子のタイプとビットマスクのタイプを区別する必要があります。以前に参照した部分的なソリューションの改善について詳しく説明する記事を次に示します。 https://dalzhim.github.io/2017/08/11/Improving-the-enum-class-bitmask/ 免責事項:私はこの後の記事の著者です。

前回の記事でテンプレートによって生成されたビットごとの演算子を使用すると、_mask == enumerator_バグをキャッチしながら、最後のコードで示したすべての利点を得ることができます。

6
Dalzhim

(広告モードがオン)便利なインターフェイスと最高のパフォーマンスの両方を取得できます。そして、タイプセーフも。 https://github.com/oliora/bitmask

1
oliora