#define、enum、または const を使用する必要がありますか?
-
02-07-2019 - |
質問
私が取り組んでいる 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;
}
スペース要件は重要ではありますが (byte か int)、重要ではありません。定義を使用すると型の安全性が失われ、 enum
一部のスペース (整数) が失われるため、ビット単位の演算を行う場合はおそらくキャストする必要があります。と const
ランダムなのでタイプセーフティも失われると思います uint8
誤って入る可能性があります。
他にもっときれいな方法はありますか?
そうでない場合、何を使用しますか?またその理由は何ですか?
追伸コードの残りの部分は、クリーンな最新の C++ です。 #define
s、そして私はいくつかのスペースで名前空間とテンプレートを使用したことがあるので、それらも問題外ではありません。
解決
戦略を組み合わせて、単一のアプローチの欠点を軽減します。私は組み込みシステムで働いているため、次の解決策は、整数演算子とビット演算子が高速で、メモリが少なく、フラッシュの使用量が少ないという事実に基づいています。
定数がグローバル名前空間を汚染するのを防ぐために、列挙型を名前空間に配置します。
namespace RecordType {
enum は、コンパイル時にチェックされる型を宣言および定義します。常にコンパイル時の型チェックを使用して、引数と変数に正しい型が指定されていることを確認します。C++ では typedef は必要ありません。
enum TRecordType { xNew = 1, xDeleted = 2, xModified = 4, xExisting = 8,
無効な状態に対して別のメンバーを作成します。これはエラー コードとして役立つ場合があります。たとえば、状態を返したいが、I/O 操作が失敗した場合です。これはデバッグにも役立ちます。初期化リストとデストラクターでこれを使用して、変数の値を使用する必要があるかどうかを確認します。
xInvalid = 16 };
このタイプには 2 つの目的があると考えてください。レコードの現在の状態を追跡し、特定の状態のレコードを選択するためのマスクを作成します。インライン関数を作成して、型の値が目的に対して有効かどうかをテストします。状態マーカーと状態マスクとして。これによりバグが捕捉されます。 typedef
はただの 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;
}
正しい値の安全性を確保する唯一の方法は、演算子オーバーロードを備えた専用クラスを使用することです。これは別の読者の演習として残されています。
他のヒント
定義は忘れてください
それらはコードを汚染します。
ビットフィールド?
struct RecordFlag {
unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};
それは絶対に使わないでください. 。4 つの int を節約することよりも速度を重視します。ビット フィールドを使用すると、実際には他のタイプにアクセスするよりも遅くなります。
ただし、構造体のビット メンバーには実際的な欠点があります。まず、メモリ内のビットの順序はコンパイラごとに異なります。加えて、 一般的なコンパイラの多くは、ビット メンバーの読み取りと書き込みに対して非効率なコードを生成します。, 、深刻な問題が発生する可能性があります スレッドの安全性の問題 これは、ほとんどのマシンがメモリ内の任意のビットのセットを操作できず、代わりにワード全体をロードして保存する必要があるため、ビット フィールド (特にマルチプロセッサ システム) に関連しています。たとえば、次のようなコードは、ミューテックスを使用しているにもかかわらず、スレッドセーフではありません。
ソース: http://en.wikipedia.org/wiki/Bit_field:
さらに理由が必要な場合は、 ない おそらくビットフィールドを使用します レイモンド・チェン 彼の言葉であなたを納得させるでしょう 古いもの、新しいもの 役職: ブール値のコレクションのビットフィールドの費用対効果の分析 で http://blogs.msdn.com/oldnewthing/archive/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 を使用する場合、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 と struct の typedef は、何よりもコードを汚染します。
結果は次のとおりです。
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.
}
}
extern const 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;
}
ただし、これらの定数をオンにすることはできません。それで最後に毒を盛るのですが…:-p
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);
bitset には多数の演算子オーバーロードがあるため、次のようにすることができます。
RecordType rt = whatever; // unsigned long or RecordType expression
rt |= xNew; // set
rt &= ~xDeleted; // clear
if ((rt & xModified) != 0) ... // test
または、それに非常に似たもの - 私はこれをテストしていないので、修正をいただければ幸いです。インデックスによってビットを参照することもできますが、通常は 1 セットの定数のみを定義することが最善であり、おそらく RecordType 定数の方が便利です。
あなたがビットセットを除外したと仮定すると、私はに投票します。 列挙型.
列挙型のキャストが重大な欠点であるという意見には同意しません。まあ、ちょっとうるさいですし、範囲外の値を列挙型に代入するのは未定義の動作なので、異常な C++ では理論的には足を撃たれる可能性があります。実装。ただし、必要な場合 (int から enum iirc に移行する場合) にのみ実行する場合、これは人々が以前に見たことのある完全に通常のコードです。
enum のスペースコストについても疑問があります。uint8 の変数とパラメーターは、おそらく int よりも少ないスタックを使用しないため、クラス内のストレージのみが重要です。構造体に複数のバイトをパッキングすると効果が得られる場合もありますが (その場合、uint8 ストレージの内外に列挙型をキャストできます)、通常はパディングによって利点が失われます。
したがって、列挙型には他の列挙型と比較して欠点はなく、利点として、多少の型安全性 (明示的にキャストせずにランダムな整数値を割り当てることはできません) と、すべてを参照するクリーンな方法が得られます。
ちなみに、優先的に列挙型に「= 2」も入れます。必須ではありませんが、「驚き最小の原則」により、4 つの定義はすべて同じに見える必要があることが示唆されています。
const と const に関する記事がいくつかあります。マクロ vs.列挙型:
新しいコードのほとんどは最新の C++ で作成されているため、特にマクロは避けるべきだと思います。
可能であればマクロは使用しないでください。最新の C++ に関しては、それらはあまり賞賛されていません。
列挙型は型安全性だけでなく「識別子に意味」も提供するため、より適切です。何年経っても「xDeleted」が「RecordType」であり、「レコードのタイプ」を表していることがはっきりわかります (すごい!)。定数にはそのためのコメントが必要であり、コード内で上下に移動する必要もあります。
定義を使用すると型の安全性が失われます
必ずしも...
// 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 が誤って入力される可能性があるため、タイプ セーフティも失われると思います。
以下のメカニズムのいずれでも同じことが発生する可能性があります。範囲と値のチェックは通常、タイプ セーフティとは直交しています (ただし、ユーザー定義の型、つまり独自のクラス - データに関する「不変条件」を強制できます)。enum を使用すると、コンパイラは値をホストするためにより大きな型を自由に選択できます。また、初期化されていない、破損している、または設定が間違っているだけの enum 変数でも、そのビット パターンが予期しない数値として解釈されてしまう可能性があります。列挙型識別子、それらの任意の組み合わせ、および 0。
他にもっときれいな方法はありますか?/ そうでない場合、何を使いますか?またその理由は何ですか?
結局のところ、ビット フィールドとカスタム演算子があれば、実績のある C スタイルの列挙型のビットごとの OR が非常にうまく機能します。mat_geek の回答のように、いくつかのカスタム検証関数とアサーションを使用して堅牢性をさらに向上させることができます。これらの手法は、文字列、整数、倍精度値などの処理にも同様に適用できることがよくあります。
これは「よりクリーン」であると主張することもできます。
enum RecordType { New, Deleted, Modified, Existing };
showRecords([](RecordType r) { return r == New || r == Deleted; });
私は無関心です:データビットはより密に詰め込まれますが、コードは大幅に増加します...持っているオブジェクトの数によって異なります。ラムバスは、それはそれで美しいですが、ビット単位の OR よりもさらに複雑で、正しく理解するのが困難です。
ところで、スレッド セーフティについての議論は私見では非常に弱いですが、主要な意思決定の推進力になるというよりも、背景的な考慮事項として記憶されるのが最もよいでしょう。ビットフィールド全体でミューテックスを共有することは、パッキングを意識していなくても実行される可能性が高くなります (ミューテックスは比較的大きなデータ メンバーです。1 つのオブジェクトのメンバーに複数のミューテックスを持つことを検討するには、パフォーマンスを非常に考慮する必要があります。慎重に検討する必要があります)それらがビットフィールドであることに気づくのに十分です)。どのサブワードサイズのタイプでも同じ問題が発生する可能性があります (例:ある uint8_t
)。とにかく、より高い同時実行性が必要な場合は、アトミックな比較およびスワップ スタイルの操作を試すことができます。
列挙型を格納するために 4 バイトを使用する必要がある場合でも (私は C++ にはあまり詳しくありません -- C# で基になる型を指定できることは知っています)、それでもそれだけの価値はあります -- 列挙型を使用してください。
GB ものメモリを備えたサーバーが存在する今日では、4 バイトと 4 バイトのようなものになります。一般に、アプリケーション レベルでの 1 バイトのメモリは問題ではありません。もちろん、特定の状況でメモリ使用量がそれほど重要である場合 (そして C++ に enum を裏付けるバイトを使用させることができない場合)、「static const」ルートを検討できます。
結局のところ、データ構造の 3 バイトのメモリを節約するために、「static const」を使用してメンテナンスに手間をかける価値はあるのか、と自問する必要があります。
他に留意すべき点 -- IIRC、x86 では、データ構造は 4 バイトでアライメントされているため、「レコード」構造内に多数のバイト幅要素がない限り、実際には問題にならない可能性があります。パフォーマンスやスペースの保守性を犠牲にする前に、テストして機能することを確認してください。
列挙構文とビット チェックの利便性を備えたクラスの型安全性が必要な場合は、次のことを検討してください。 C++ の安全なラベル. 。著者と一緒に仕事をしたことがありますが、彼はとても賢い人です。
ただし、注意してください。結局、このパッケージはテンプレートを使用します そして マクロ!
実際に概念的な全体としてフラグ値を渡す必要があるのでしょうか、それともフラグごとのコードが大量に必要になるのでしょうか?いずれにせよ、これを 1 ビットのビットフィールドのクラスまたは構造体として持つ方が、実際にはより明確になる可能性があると思います。
struct RecordFlag {
unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};
そうすれば、レコード クラスに struct RecordFlag メンバー変数を含めることができ、関数は struct RecordFlag 型の引数を取ることができます。コンパイラはビットフィールドをまとめてパックし、スペースを節約する必要があります。
私はおそらく、値を組み合わせることができるこの種のものには列挙型を使用しないでしょう。より一般的に列挙型は相互に排他的な状態です。
ただし、どの方法を使用する場合でも、これらが結合可能なビットであることをより明確にするために、実際の値には代わりに次の構文を使用します。
#define X_NEW (1 << 0)
#define X_DELETED (1 << 1)
#define X_MODIFIED (1 << 2)
#define X_EXISTING (1 << 3)
そこで左シフトを使用すると、各値が単一ビットであることを意図していることを示すことができ、後で誰かが新しい値を追加してそれに値 9 を割り当てるなどの間違った操作を行う可能性が低くなります。
に基づく キス, 高い凝集性と低い結合性, 、これらの質問をしてください -
- 誰が知る必要があるでしょうか?私のクラス、私のライブラリ、他のクラス、他のライブラリ、サードパーティ
- どのレベルの抽象化を提供する必要がありますか?消費者はビット操作を理解していますか。
- VB/C#などからインターフェースする必要がありますか?
すごい本がありますよ」大規模な C++ ソフトウェア設計"、これにより、基本型が外部に昇格されます。別のヘッダー ファイル/インターフェイスの依存関係を回避できる場合は、そうする必要があります。
Qt を使用している場合は、次のものを探してください。 Qフラグ。QFlags クラスは、列挙値の OR の組み合わせを保存するタイプセーフな方法を提供します。
むしろ一緒に行きたいです
typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;
単純に〜だから:
- これにより、コードがよりクリーンになり、読みやすく、保守しやすくなります。
- 定数を論理的にグループ化します。
- 自分の仕事を除いて、プログラマーの時間の方が重要です は これらの 3 バイトを保存します。
すべてをオーバーエンジニアリングするのが好きというわけではありませんが、場合によっては、この情報をカプセル化する (小さな) クラスを作成する価値があるかもしれません。RecordType クラスを作成すると、次のような関数が含まれる可能性があります。
void setDeleted();
void クリア削除();
bool isDeleted();
等...(または慣例に適したものなら何でも)
組み合わせを検証することもできます (すべての組み合わせが正当であるわけではない場合、たとえば、「新規」と「削除」の両方を同時に設定できない場合)。ビット マスクなどを使用しただけの場合は、状態を設定するコードを検証する必要があります。クラスはそのロジックもカプセル化できます。
このクラスでは、意味のあるログ情報を各状態に添付する機能も提供され、現在の状態などの文字列表現を返す関数を追加することもできます (またはストリーミング演算子 '<<' を使用します)。
それでも、ストレージが心配な場合は、クラスに「char」データ メンバーのみを持たせることもできるため、少量のストレージのみを使用します (仮想ストレージではないと仮定して)。もちろん、ハードウェアなどによっては、位置合わせの問題が発生する可能性があります。
実際のビット値がヘッダー ファイルではなく cpp ファイル内の匿名名前空間にある場合は、残りの「世界」には表示されない可能性があります。
enum/#define/ビットマスクなどを使用するコードに、無効な組み合わせやログ記録などを処理するための「サポート」コードが多数含まれていることがわかった場合は、クラスでのカプセル化を検討する価値があるかもしれません。もちろん、ほとんどの場合、単純な問題には単純な解決策を使用する方が良いでしょう...