126

私が取り組んでいるC++プロジェクトでは、 4つの値を持つことができるフラグの種類の値があります。これらの4つのフラグを組み合わせることができます。フラグはデータベース内のレコードを記述し、次のようになります。

  • 新記録
  • 削除されたレコード
  • 変更されたレコード
  • 既存の記録

ここで、レコードごとにこの属性を保持したいので、列挙型を使用できます。

enum { xNew, xDeleted, xModified, xExisting }

ただし、コードの他の場所では、ユーザーに表示するレコードを選択する必要があるため、次のように、それを単一のパラメーターとして渡すことができるようにしたいと思います。

showRecords(xNew | xDeleted);

だから、私には3つの可能なアプローチがあるようです:

#define X_NEW      0x01
#define X_DELETED  0x02
#define X_MODIFIED 0x04
#define X_EXISTING 0x08

また

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

また

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

スペース要件は重要です(バイト対整数)が、重要ではありません。定義を使用すると型の安全性がenum失われ、スペース(整数)が失われるため、ビット演算を実行するときにキャストする必要があります。ランダムが誤って侵入する可能性がconstあるため、私も型安全性を失うと思います。uint8

他にもっときれいな方法はありますか?

そうでない場合は、何を使用しますか、またその理由は何ですか。

PS残りのコードは、#definesのない最新のC ++でかなりクリーンであり、名前空間とテンプレートをいくつかのスペースで使用しているので、それらも問題ありません。

4

15 に答える 15

90

戦略を組み合わせて、単一のアプローチの欠点を減らします。私は組み込みシステムで作業しているので、次のソリューションは、整数演算子とビット演算子が高速で、メモリが少なく、フラッシュの使用量が少ないという事実に基づいています。

定数がグローバル名前空間を汚染しないように、列挙型を名前空間に配置します。

namespace RecordType {

列挙型は、チェック型のコンパイル時を宣言および定義します。常にコンパイル時の型チェックを使用して、引数と変数に正しい型が指定されていることを確認してください。C++ではtypedefは必要ありません。

enum TRecordType { xNew = 1, xDeleted = 2, xModified = 4, xExisting = 8,

無効な状態の別のメンバーを作成します。これはエラーコードとして役立ちます。たとえば、状態を返したいがI/O操作が失敗した場合です。デバッグにも役立ちます。初期化リストとデストラクタでこれを使用して、変数の値を使用する必要があるかどうかを確認します。

xInvalid = 16 };

このタイプには2つの目的があると考えてください。レコードの現在の状態を追跡し、特定の状態のレコードを選択するためのマスクを作成します。タイプの値が目的に有効かどうかをテストするインライン関数を作成します。状態マーカーと状態マスクとして。これはバグをキャッチします。これは、typedefが単なるanであり、初期化されていない変数や誤った変数を介して変数にint含まれる可能性のある値などです。0xDEADBEEF

inline bool IsValidState( TRecordType v) {
    switch(v) { case xNew: case xDeleted: case xModified: case xExisting: return true; }
    return false;
}

 inline bool IsValidMask( TRecordType v) {
    return v >= xNew  && v < xInvalid ;
}

usingタイプを頻繁に使用する場合は、ディレクティブを追加します。

using RecordType ::TRecordType ;

値チェック関数は、不正な値が使用されるとすぐにトラップするためのアサートに役立ちます。実行中にバグをすばやくキャッチするほど、発生する可能性のあるダメージは少なくなります。

これをすべてまとめる例をいくつか示します。

void showRecords(TRecordType mask) {
    assert(RecordType::IsValidMask(mask));
    // do stuff;
}

void wombleRecord(TRecord rec, TRecordType state) {
    assert(RecordType::IsValidState(state));
    if (RecordType ::xNew) {
    // ...
} in runtime

TRecordType updateRecord(TRecord rec, TRecordType newstate) {
    assert(RecordType::IsValidState(newstate));
    //...
    if (! access_was_successful) return RecordType ::xInvalid;
    return newstate;
}

正しい値の安全性を確保する唯一の方法は、演算子のオーバーロードを伴う専用クラスを使用することであり、それは別の読者の演習として残されています。

于 2008-09-22T07:09:14.143 に答える
57

定義を忘れる

それらはあなたのコードを汚染します。

ビットフィールド?

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

それを使用しないでください。あなたは 4 int を節約するよりもスピードに関心があります。ビット フィールドを使用すると、実際には他の型へのアクセスよりも遅くなります。

ただし、構造体のビット メンバーには実際的な欠点があります。まず、メモリ内のビットの順序はコンパイラごとに異なります。さらに、多くの一般的なコンパイラは、ビット メンバーの読み取りと書き込みに対して非効率なコードを生成します。また、ほとんどのマシンがメモリ内のビットの任意のセットを操作できないため、ビット フィールドに関連する深刻なスレッド セーフの問題が発生する可能性があります (特にマルチプロセッサ システムでは)。代わりに、単語全体をロードして保存する必要があります。たとえば、ミューテックスを使用しているにもかかわらず、以下はスレッドセーフではありません。

ソース: http://en.wikipedia.org/wiki/Bit_field :

また、ビットフィールドを使用しない理由がさらに必要な場合はRaymond ChenがThe Old New Thingの投稿であなたを納得させるでしょう: http://blogs.msdn.com/oldnewthing/アーカイブ/2008/11/26/9143050.aspx

定数整数?

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

それらを名前空間に入れるのはクールです。それらが CPP またはヘッダー ファイルで宣言されている場合、それらの値はインライン化されます。これらの値でスイッチを使用できますが、カップリングがわずかに増加します。

ああ、はい: static キーワードを削除してください。static は、C++ で使用する場合は非推奨です。uint8 が組み込み型の場合、同じモジュールの複数のソースに含まれるヘッダーでこれを宣言する必要はありません。最終的に、コードは次のようになります。

namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

このアプローチの問題は、コードが定数の値を認識しているため、結合がわずかに増加することです。

列挙

const int と同じですが、型付けが多少強くなります。

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

ただし、それらは依然としてグローバル名前空間を汚染しています。ちなみに... typedef を削除します。あなたは C++ で作業しています。これらの列挙型と構造体の型定義は、何よりもコードを汚染しています。

結果はちょっと:

enum RecordType { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;

void doSomething(RecordType p_eMyEnum)
{
   if(p_eMyEnum == xNew)
   {
       // etc.
   }
}

ご覧のとおり、列挙型はグローバル名前空間を汚染しています。この列挙型を名前空間に配置すると、次のようになります。

namespace RecordType {
   enum Value { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;
}

void doSomething(RecordType::Value p_eMyEnum)
{
   if(p_eMyEnum == RecordType::xNew)
   {
       // etc.
   }
}

外部定数 int ?

結合を減らしたい場合 (つまり、定数の値を非表示にして、完全な再コンパイルを必要とせずに必要に応じて変更できるようにする場合)、int をヘッダーで extern として宣言し、CPP ファイルで定数として宣言できます。 、次の例のように:

// Header.hpp
namespace RecordType {
    extern const uint8 xNew ;
    extern const uint8 xDeleted ;
    extern const uint8 xModified ;
    extern const uint8 xExisting ;
}

と:

// Source.hpp
namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

ただし、これらの定数に対して switch を使用することはできません。最後に、あなたの毒を選んでください... :-p

于 2008-09-21T23:52:53.160 に答える
30

std::bitset を除外しましたか? フラグのセットがその目的です。行う

typedef std::bitset<4> RecordType;

それから

static const RecordType xNew(1);
static const RecordType xDeleted(2);
static const RecordType xModified(4);
static const RecordType xExisting(8);

ビットセットには多数の演算子のオーバーロードがあるため、次のことができます。

RecordType rt = whatever;      // unsigned long or RecordType expression
rt |= xNew;                    // set 
rt &= ~xDeleted;               // clear 
if ((rt & xModified) != 0) ... // test

またはそれに非常に似たもの-これをテストしていないので、修正をいただければ幸いです。インデックスでビットを参照することもできますが、通常は 1 セットの定数のみを定義するのが最善であり、RecordType 定数の方がおそらくより便利です。

bitset を除外したと仮定すると、私はenumに投票します。

列挙型のキャストが深刻な欠点であることは同意しません-OKなので、少し騒がしく、列挙型に範囲外の値を割り当てることは未定義の動作であるため、理論的には、いくつかの異常なC ++で自分自身を撃つことが可能です実装。しかし、必要な場合 (int から enum iirc に移行する場合) にのみ実行する場合は、これまでに見たことのある完全に正常なコードです。

列挙型のスペースコストについても疑わしいです。uint8 の変数とパラメーターは、おそらく int より少ないスタックを使用することはないため、クラス内のストレージのみが問題になります。構造体に複数のバイトをパッキングする方が有利な場合もあります (この場合、列挙型を uint8 ストレージの内外にキャストできます) が、通常はパディングによってメリットが失われます。

したがって、列挙型には他のものと比較して欠点がなく、利点として、少しの型安全性 (明示的にキャストしないとランダムな整数値を割り当てることはできません) と、すべてを参照するクリーンな方法が得られます。

ちなみに、私は「= 2」も列挙型に入れたいと思います。必須ではありませんが、「最小の驚きの原則」により、4 つの定義はすべて同じに見えるはずです。

于 2008-09-21T23:06:16.347 に答える
8

const vs. macros vs. enums に関するいくつかの記事を次に示します。

シンボリック定数
列挙型定数と定数オブジェクト

特に、新しいコードのほとんどを最新の C++ で作成したため、マクロは避けるべきだと思います。

于 2008-09-21T23:01:03.487 に答える
5

可能であれば、マクロを使用しないでください。最新の C++ に関して言えば、彼らはあまり賞賛されていません。

于 2008-09-22T06:30:24.000 に答える
4

列挙型は、「識別子に意味」と型の安全性を提供するため、より適切です。「xDeleted」が「RecordType」であり、「レコードのタイプ」を表していることがはっきりとわかります (うわー!) 何年経っても。const にはそのためのコメントが必要であり、コード内で上下に移動する必要もあります。

于 2008-09-21T22:57:33.230 に答える
4

定義すると、型の安全性が失われます

必ずしも...

// signed defines
#define X_NEW      0x01u
#define X_NEW      (unsigned(0x01))  // if you find this more readable...

そしてenumを使用すると、スペース(整数)が失われます

必ずしもそうではありませんが、ストレージのポイントで明示する必要があります...

struct X
{
    RecordType recordType : 4;  // use exactly 4 bits...
    RecordType recordType2 : 4;  // use another 4 bits, typically in the same byte
    // of course, the overall record size may still be padded...
};

ビット単位の操作を行いたい場合は、おそらくキャストする必要があります。

オペレーターを作成して、その苦痛を取り除くことができます。

RecordType operator|(RecordType lhs, RecordType rhs)
{
    return RecordType((unsigned)lhs | (unsigned)rhs);
}

const を使用すると、ランダムな uint8 が誤って入る可能性があるため、型の安全性も失われると思います。

これらのメカニズムのいずれでも同じことが起こり得ます: 範囲と値のチェックは、通常、型の安全性と直交しています (ただし、ユーザー定義型 (つまり、独自のクラス) は、そのデータについて「不変条件」を適用できます)。列挙型を使用すると、コンパイラは値をホストするためにより大きな型を自由に選択できます。また、初期化されていない、破損している、または設定ミスの列挙型変数は、ビットパターンを予期しない数値として解釈する可能性があります-どの値とも等しくない比較列挙識別子、それらの任意の組み合わせ、および 0。

他のクリーンな方法はありますか?/ そうでない場合、何を使用しますか?その理由は何ですか?

最終的には、ビット フィールドとカスタム演算子が図にあると、実証済みの信頼できる C スタイルの列挙型のビットごとの OR が非常にうまく機能します。mat_geek の回答のように、いくつかのカスタム検証関数とアサーションを使用して、堅牢性をさらに向上させることができます。多くの場合、string、int、double 値などの処理にも同じように適用できる手法です。

これは「よりクリーン」であると主張できます。

enum RecordType { New, Deleted, Modified, Existing };

showRecords([](RecordType r) { return r == New || r == Deleted; });

私は無関心です:データビットはより密集しますが、コードは大幅に大きくなります...オブジェクトの数に依存し、lamdbas(それらは美しいですが)は依然として乱雑で、ビットごとの OR よりも正しく取得するのが困難です。

ところで /- スレッド セーフのかなり弱い IMHO に関する議論 - 支配的な意思決定の原動力になるのではなく、背景の考慮事項として覚えておくのが最善です。パッキングを認識していなくても、ビットフィールド全体でミューテックスを共有する可能性が高くなります(ミューテックスは比較的かさばるデータメンバーです-1つのオブジェクトのメンバーに複数のミューテックスを持つことを検討するには、パフォーマンスについて本当に心配する必要があります。慎重に調べますそれらがビットフィールドであることに気付くのに十分です)。任意のサブワード サイズの型でも同じ問題が発生する可能性があります (例: a uint8_t)。とにかく、より高い同時実行性がどうしても必要な場合は、アトミックな比較と交換スタイルの操作を試すことができます。

于 2012-08-07T13:24:37.303 に答える
3

列挙型を格納するために4バイトを使用する必要がある場合でも(C ++にはあまり詳しくありません。C#で基になる型を指定できることはわかっています)、それでも価値があります。列挙型を使用してください。

GBのメモリを搭載したサーバーの時代では、一般に、アプリケーションレベルでの4バイトと1バイトのメモリのようなものは重要ではありません。もちろん、特定の状況でメモリ使用量が非常に重要である場合(そして、C ++で列挙型をバックアップするためにバイトを使用することができない場合)、「staticconst」ルートを検討できます。

結局のところ、データ構造の3バイトのメモリ節約のために「staticconst」を使用することのメンテナンスヒットの価値があるかどうかを自問する必要がありますか?

他に覚えておくべきことがあります-IIRC、x86では、データ構造は4バイトで整列されるため、「レコード」構造に多数のバイト幅要素がない限り、実際には問題にならない可能性があります。パフォーマンス/スペースの保守性のトレードオフを行う前に、テストして確認してください。

于 2008-09-21T22:54:24.767 に答える
3

列挙構文とビット チェックの利便性を備えたクラスのタイプ セーフが必要な場合は、 C++ のセーフ ラベルを検討してください。私は著者と仕事をしたことがありますが、彼はかなり頭がいいです。

ただし、注意してください。最後に、このパッケージはテンプレートマクロを使用します!

于 2008-09-22T02:05:27.383 に答える
2

実際にフラグ値を概念全体として渡す必要がありますか、それともフラグごとのコードがたくさんあるのでしょうか。いずれにせよ、これを1ビットビットフィールドのクラスまたは構造体として持つ方が実際にはより明確かもしれないと思います。

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

次に、レコードクラスにstruct RecordFlagメンバー変数を含めることができ、関数はstruct RecordFlag型の引数を取ることができます。コンパイラはビットフィールドをまとめてパックし、スペースを節約する必要があります。

于 2008-09-21T23:15:55.620 に答える
2

Qt を使用している場合は、QFlagsを探す必要があります。QFlags クラスは、列挙値の OR 組み合わせを格納するタイプ セーフな方法を提供します。

于 2008-09-22T06:26:03.450 に答える
2

値を組み合わせることができるこの種のものには、おそらく列挙型を使用しないでしょう。より一般的には、列挙型は相互に排他的な状態です。

ただし、どちらの方法を使用しても、これらが組み合わせ可能なビットである値であることをより明確にするために、代わりに実際の値に次の構文を使用します。

#define X_NEW      (1 << 0)
#define X_DELETED  (1 << 1)
#define X_MODIFIED (1 << 2)
#define X_EXISTING (1 << 3)

ここで左シフトを使用すると、各値が 1 ビットであることを示すのに役立ちます。後で誰かが新しい値を追加して値 9 を割り当てるなど、何か間違ったことをする可能性は低くなります。

于 2008-09-22T02:03:37.997 に答える
2

KISS高い結束力と低い結合度に基づいて、これらの質問をしてください -

  • 誰が知る必要がありますか? 私のクラス、私のライブラリ、他のクラス、他のライブラリ、サードパーティ
  • どのレベルの抽象化を提供する必要がありますか? 消費者はビット操作を理解していますか。
  • VB/C# などからインターフェイスする必要がありますか?

優れた本「Large-Scale C++ Software Design」があります。これは、別のヘッダー ファイル/インターフェイスの依存関係を避けることができる場合は、基本型を外部に昇格することを試みる必要があります。

于 2008-09-22T05:58:03.517 に答える
0

私はむしろ行きたい

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

単純に〜だから:

  1. それはよりクリーンで、コードを読みやすく、保守しやすくします。
  2. 定数を論理的にグループ化します。
  3. あなたの仕事がそれらの3バイトを保存すること でない限り、プログラマーの時間はより重要です。
于 2008-09-21T23:27:00.150 に答える
0

すべてを過度に設計するのが好きというわけではありませんが、このような場合、この情報をカプセル化する (小さな) クラスを作成する価値がある場合があります。クラス RecordType を作成すると、次のような関数が含まれる場合があります。

void setDeleted();

void clearDeleted();

bool isDeleted();

など...(またはどんな慣習が合っていても)

組み合わせを検証できます (すべての組み合わせが有効であるとは限らない場合、たとえば、'new' と 'deleted' の両方を同時に設定できない場合など)。ビットマスクなどを使用しただけの場合、状態を設定するコードを検証する必要があり、クラスはそのロジックもカプセル化できます。

クラスは、意味のあるログ情報を各状態に添付する機能も提供する場合があります。現在の状態などの文字列表現を返す関数を追加できます (またはストリーミング演算子 '<<' を使用します)。

ストレージについて心配している場合でも、クラスに「char」データメンバーのみを持たせることができるため、少量のストレージのみを使用してください(仮想ではない場合)。もちろん、ハードウェアなどによっては、アライメントの問題が発生する場合があります。

実際のビット値が、ヘッダー ファイルではなく cpp ファイル内の匿名名前空間にある場合、「世界」の残りの部分には見えない可能性があります。

enum/#define/ bitmask などを使用するコードに、無効な組み合わせ、ログなどを処理するための「サポート」コードが多数ある場合は、クラスへのカプセル化を検討する価値があります。もちろん、ほとんどの場合、単純な問題は単純な解決策の方が適しています...

于 2008-09-22T12:41:50.300 に答える