C および C++ におけるユニオンの目的
-
22-09-2019 - |
質問
私は以前にunionを快適に使用しました。今日読んでいてびっくりしました この郵便受け このコードがあることを知りました
union ARGB
{
uint32_t colour;
struct componentsTag
{
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
} components;
} pixel;
pixel.colour = 0xff040201; // ARGB::colour is the active member from now on
// somewhere down the line, without any edit to pixel
if(pixel.components.a) // accessing the non-active member ARGB::components
実際には未定義の動作です。最近書き込まれたユニオンのメンバー以外のメンバーから読み取ると、未定義の動作が発生します。これが共用体の意図された使用法ではないとしたら、どういうことなのでしょうか?誰か詳しく説明してもらえませんか?
アップデート:
後になって考えて、いくつかのことを明確にしたいと思いました。
- この質問に対する答えは、C と C++ で同じではありません。無知な若い頃の私は、これを C と C++ の両方としてタグ付けしていました。
- C++11 の標準を精査した結果、非アクティブな共用体メンバーへのアクセス/検査が未定義/未指定/実装定義であると結論付けることができませんでした。私が見つけられたのは§9.5/1 だけでした。
標準レイアウト共用体に、共通の初期シーケンスを共有する複数の標準レイアウト構造体が含まれており、この標準レイアウト共用体タイプのオブジェクトに標準レイアウト構造体の 1 つが含まれている場合、任意の標準レイアウト構造体の共通初期シーケンスを検査することが許可されます。標準レイアウトの構造体メンバーの数。§9.2/19:2 つの標準レイアウト構造体は、対応するメンバーにレイアウト互換型があり、どちらのメンバーもビットフィールドではないか、1 つ以上の初期メンバーのシーケンスで両方とも同じ幅のビットフィールドである場合、共通の初期シーケンスを共有します。
- C にいる間、(C99 TC3 - DR 283 以降) そうすることは合法です (パスカル・クック氏に感謝 これを取り上げたことに対して)。ただし、やろうとすると、 まだ未定義の動作が発生する可能性があります, 、読み取られた値がたまたまその型に対して無効であった場合 (いわゆる「トラップ表現」)、その値は読み取られます。それ以外の場合、読み取られる値は実装定義です。
C89/90 はこれを未指定の動作 (付録 J) として呼び出し、K&R の本には実装定義であると記載されています。K&R からの引用:
これが共用体の目的です。つまり、複数の型のいずれかを正当に保持できる単一の変数です。[...] 使用法が一貫している限り:取得される型は、最後に保存された型である必要があります。現在どの型が共用体に格納されているかを追跡するのはプログラマの責任です。何かが 1 つの型として保存され、別の型として抽出される場合、結果は実装に依存します。
Stroustrup の TC++PL からの抜粋 (強調は私のもの)
共用体の使用は、データの互換性を保つために不可欠な場合があります [...] 「型変換」という意味で誤用されることもあります".
何よりも、この質問(タイトルは私の質問以来変わっていません)は、標準で何が許可されているかではなく、組合の目的を理解することを目的として提起されました。 例えば。コードを再利用するために継承を使用することは、もちろん C++ 標準で許可されていますが、 それは C++ 言語機能として継承を導入する目的や本来の意図ではありませんでした. 。これが、アンドレイの答えが受け入れられ続けている理由です。
解決
組合の目的はかなり明白ですが、何らかの理由で人々はそれを見逃してしまうことがよくあります。
組合の目的は、 メモリを節約するために 同じメモリ領域を使用して、異なる時点で異なるオブジェクトを保存します。 それでおしまい。
まるでホテルの一室のようです。さまざまな人々が、重複しない期間、その中に住んでいます。これらの人々は一度も会うことはなく、通常はお互いのことを何も知りません。部屋のタイムシェアリングを適切に管理することで(つまり、異なる人が同時に 1 つの部屋に割り当てられないようにすることで)、比較的小規模なホテルでも比較的多数の人に宿泊施設を提供でき、それがホテルの目的です。
まさに労働組合がやっていることです。プログラム内の複数のオブジェクトが値の有効期間が重複しない値を保持していることがわかっている場合は、これらのオブジェクトを共用体に「マージ」することでメモリを節約できます。ホテルの部屋に各時点で最大 1 人の「アクティブ」テナントがいるのと同じように、組合にはプログラム時間の各時点で最大 1 人の「アクティブ」メンバーがいます。「アクティブな」メンバーのみを読み取ることができます。他のメンバーに書き込むと、その他のメンバーの「アクティブ」ステータスが切り替わります。
何らかの理由で、この結合の本来の目的は、まったく異なるもので「オーバーライド」されました。組合のメンバーの 1 人を書き込み、それを別のメンバーを通じて検査します。この種の記憶の再解釈 (別名「型パニング」) は、 共用体の有効な使用法ではありません。通常、未定義の動作が発生します C89/90 では実装定義の動作を生成すると説明されています。
編集: 型の語呂合わせの目的で共用体を使用する (つまり、1 つのメンバーを書き込み、次に別のメンバーを読み取る) は、C99 標準の技術的正誤表の 1 つでより詳細な定義が与えられました (「参照」を参照)。 DR#257 そして DR#283)。ただし、正式には、これはトラップ表現を読み取ろうとすることによる未定義の動作の発生を防ぐものではないことに注意してください。
他のヒント
あなたは労働組合の構成要素が実際に使用されるがわかりますフィールドが含まれている以下のような構造体を作成するために労働組合を使用することができます
struct VAROBJECT
{
enum o_t { Int, Double, String } objectType;
union
{
int intValue;
double dblValue;
char *strValue;
} value;
} object;
は動作がビューの言語の観点から定義されていません。異なるプラットフォームは、メモリアライメントとエンディアンで異なる制約を持つことができることを考えてみましょう。リトルエンディアンのマシン対ビッグエンディアンのコードは、異なる構造体の値を更新します。使用を制限(...とメモリ整列制約)言語で動作を修正同じエンディアンを使用するすべての実装が必要になります。
あなたは(あなたが二つのタグを使用している)、C ++を使用していて、本当に移植性を気にした場合、その後、あなただけの構造体を使用してuint32_t
を取り、適切なビットマスク操作でフィールドを設定し、セッターを提供することができます。同じ機能を有するCで行うことができます。
編集:私は投票への回答と近く、このいずれかを書き留めてAProgrammerを期待していました。いくつかのコメントが指摘したように、エンディアンは、各実装が何をすべきかを決定させることで、標準の他の部分で配られ、アライメントおよびパディングも異なる方法で処理することができます。さて、AProgrammerが暗黙的に参照する、厳密なエイリアシング規則は、ここで重要なポイントです。コンパイラは、変数の変更(または変更の欠如)に仮定を行うことが許可されています。組合の場合には、コンパイラは、命令を並べ替えることができ、色変数への書き込み上に各色成分のリードを移動させる。
私は定期的に遭遇union
の最もの共通の使用はのエイリアシングのです。
次のことを考えてみます:
union Vector3f
{
struct{ float x,y,z ; } ;
float elts[3];
}
これは何をしますか?
:これはのいずれかの名前でVector3f vec;
のメンバーのきれいな、きちんとしたアクセスを可能にします
vec.x=vec.y=vec.z=1.f ;
または配列に整数アクセスによる
for( int i = 0 ; i < 3 ; i++ )
vec.elts[i]=1.f;
いくつかのケースでは、名前でアクセスすることはあなたが行うことができます明確なものです。他の場合には、特に軸がプログラム選択された場合、実行する簡単なものは、数値インデックスによって軸にアクセスすることである - yのxの0、1、及び2をZための
は、これは、厳密には未定義の動作です。労働組合を使用するための本当の理由は、バリアントレコードを作成することです。
union A {
int i;
double d;
};
A a[10]; // records in "a" can be either ints or doubles
a[0].i = 42;
a[1].d = 1.23;
もちろん、あなたもバリエーションが実際に含まれているものを言うの弁別のいくつかの並べ替えを必要とします。 C ++組合はあまり使用されている中で、彼らは唯一のPODタイプを含めることができるので、ノートという - 。効果的にコンストラクタとデストラクタのないものを
でCそれバリアントのようなものを実装するための良い方法でした。
enum possibleTypes{
eInt,
eDouble,
eChar
}
struct Value{
union Value {
int iVal_;
double dval;
char cVal;
} value_;
possibleTypes discriminator_;
}
switch(val.discriminator_)
{
case eInt: val.value_.iVal_; break;
litlleメモリの回では、この構造は、すべての部材を有する構造体より少ないメモリを使用しています。
Cは
提供ところで typedef struct {
unsigned int mantissa_low:32; //mantissa
unsigned int mantissa_high:20;
unsigned int exponent:11; //exponent
unsigned int sign:1;
} realVal;
アクセス・ビット値である。
、実際にはかなり上の任意のコンパイラで動作します。任意の自己尊重コンパイラはこのような例では、「正しいこと」を実行する必要がありますようなA広く使われているパラダイムです。これはよくいくつかのコンパイラで壊れたコードを生成することがあり、タイプpunning、より優先されるように、確かです。
ではC ++、バリアントが安全を実装ブーストできるだけ未定義の動作を防ぐために設計された労働組合のバージョンます。
その性能は(スタックすぎなどに割り当てられた)enum + union
構文と同じですが、それは代わりにenum
の種類のテンプレートのリストを使用しています)。
は動作が不定になることがありますが、それは単に「標準」がないことを意味します。すべてのまともなコンパイラはの#pragmaするに提供します梱包および配向を制御が、異なるデフォルト値を有することができます。デフォルト値を使用しても、最適化の設定に応じて変化します。
また、労働組合はありません。のちょうどのスペースを節約するため。彼らは、タイプpunningと近代的なコンパイラを助けることができます。あなたreinterpret_cast<>
すべてが、コンパイラは、あなたが何をしているかについての仮定を行うことができない場合。それはそれはあなたのタイプを知っているものを捨てて(これらの日CPUのクロック速度に比べて非常に非効率的であるメモリへの書き戻しを、強制的に)再起動する必要があります。
技術的には未定義だが、実際には、ほとんど(すべて?)コンパイラは、実装定義されているの他、その結果、1つのタイプからreinterpret_cast
を使用するのとまったく同じ扱い。私はあなたの現在のコード終わっていない失う睡眠でしょう。
は組合の実際の使用のもう一つの例は、CORBAフレームワークは、タグ付けされた組合のアプローチを使用してオブジェクトをシリアル化します。すべてのユーザ定義のクラスは、一つ(巨大)組合のメンバーであり、そして整数識別子の指示します組合をどのように解釈するかdemarshallerます。
その他のアーキテクチャの違い言及している。(リトル - ビッグエンディアンを)
私は、変数のためのメモリが共有されているので、1に書き込むことで、他の人が変更して、そのタイプに応じて、その後、値は無意味かもしれないという問題を読みます。
など。 連合{ フロートF; 私はint型。 } X;
あなたがx.fから読めばx.iへの書き込みは意味がありません - 。それはあなたがフロートの符号、指数または仮数部の部品を見るために意図したものでない限り、
私はアライメントの問題もあると思う:。いくつかの変数はワード境界でなければならないならば、あなたが期待する結果を得られない可能性があります。
など。 連合{ チャーC [4]。 私はint型。 } X;
の場合には、仮に、いくつかのマシン上でチャーがワード整列しなければならなかったしてc [0]、C [1]私はなく、C [2]、C [3]でストレージを共有することになる。
C言語では、それが1974年に文書化されたように、すべての構造体のメンバは、共通の名前空間を共有し、そして「ptr->メンバー」の意味は、追加として、の定義されたのでした 「PTR」と使用したアドレスにアクセスするメンバーの変位 メンバーのタイプ。このデザインはメンバーと同じPTRを使用することが可能になりました 異なる構造の定義からではなく、同じオフセットで撮影した名前。 プログラマは、様々な目的のためにその能力を使用します。
構造体のメンバは、独自の名前空間を割り当てた場合は、、それができなくなりました 同一の変位を有する2つの構造体のメンバを宣言します。に労働組合を追加します 言語は、それが可能であったのと同じセマンティクスを達成するために作られました できないことは持っているものの(言語の以前のバージョンで利用可能 外側のコンテキストにエクスポート名前はまだAを使用して必要としていること )を見つける/ foo-> type1.memberにfoo->メンバーを交換する交換してください。何だった 重要なので、多くの労働組合を追加したユーザーが任意の特定を持っていることはなかったです 心の中でターゲットの使用ではなく、彼らはどのプログラマー手段を提供することを 誰が何の目的のために、以前の意味論に依拠を持っていた。の、まだすべき 彼らは別のを使用していた場合でも、同じセマンティクスを達成することができます それを行うための構文ます。
あなたはできる 使用 a 結合には主に次の 2 つの理由があります。
- あなたの例のように、さまざまな方法で同じデータにアクセスする便利な方法
- 異なるデータ メンバーがあり、そのうちの 1 つだけを「アクティブ」にできる場合にスペースを節約する方法
1 実際には、ターゲット システムのメモリ アーキテクチャがどのように機能するかを知っていることに基づいて、コードをショートカットで作成するための C スタイルのハックです。すでに述べたように、実際に多くの異なるプラットフォームをターゲットにしていない場合は、通常は問題なく対処できます。一部のコンパイラではパッキング ディレクティブも使用できると思います (構造体でも使用できることは知っています)。
2の良い例です。で見つけることができます 変異体 COM で広く使用されるタイプ。
、組合列挙と合わせ、構造体にラップは、タグ付けされた組合を実装するために使用することができます。一つの実用化は、もともと純粋Result<T, E>
を使用して実装されて錆のenum
を、(錆が列挙バリアントで追加データを保持することができます)を実装することです。ここではC ++の例があります:
template <typename T, typename E> struct Result {
public:
enum class Success : uint8_t { Ok, Err };
Result(T val) {
m_success = Success::Ok;
m_value.ok = val;
}
Result(E val) {
m_success = Success::Err;
m_value.err = val;
}
inline bool operator==(const Result& other) {
return other.m_success == this->m_success;
}
inline bool operator!=(const Result& other) {
return other.m_success != this->m_success;
}
inline T expect(const char* errorMsg) {
if (m_success == Success::Err) throw errorMsg;
else return m_value.ok;
}
inline bool is_ok() {
return m_success == Success::Ok;
}
inline bool is_err() {
return m_success == Success::Err;
}
inline const T* ok() {
if (is_ok()) return m_value.ok;
else return nullptr;
}
inline const T* err() {
if (is_err()) return m_value.err;
else return nullptr;
}
// Other methods from https://doc.rust-lang.org/std/result/enum.Result.html
private:
Success m_success;
union _val_t { T ok; E err; } m_value;
}