組合を以前より快適に使用しました。今日 this post を読んだときに不安になり、このコードを知った
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
実際には未定義の動作です。最近書かれたもの以外の組合員から読むと、未定義の行動につながります。これが組合の意図された使用法ではない場合、何ですか?誰かがそれを詳しく説明してもらえますか?
更新:
私は後知恵でいくつかのことを明確にしたかった。
標準レイアウト共用体に、共通の初期シーケンスを共有する複数の標準レイアウト構造体が含まれ、この標準レイアウト共用体タイプのオブジェクトに標準レイアウト構造体の1つが含まれる場合、任意の共通初期シーケンスを検査できます。標準レイアウト構造体メンバーの。 §9.2/ 19:2つの標準レイアウト構造体は、対応するメンバーにレイアウト互換型があり、どちらのメンバーもビットフィールドでないか、両方が1つ以上のイニシャルのシーケンスに対して同じ幅のビットフィールドである場合、共通の初期シーケンスを共有しますメンバー。
C89/90は不特定の動作(Annex J)でこれを呼び出し、K&Rの本はそれが定義された実装であると述べています。 K&Rからの引用:
これがユニオンの目的です-いくつかのタイプのいずれかを合法的に保持できる単一の変数。 [...]使用法が一貫している限り:取得される型は、最後に保存された型でなければなりません。どのタイプが現在ユニオンに格納されているかを追跡するのは、プログラマーの責任です。何かが1つのタイプとして保存され、別のタイプとして抽出される場合、結果は実装に依存します。
StroustrupのTC++ PL(強調鉱山)からの抽出
ユニオンの使用は、データの互換性に不可欠な場合があります[...]「型変換」に誤用される場合があります。
何よりも、この質問(タイトルは私の依頼以来変更されていない)は、組合の目的を理解することを意図しており、標準が許すものではないE.g.もちろん、コードの再利用に継承を使用することはC++標準で許可されていますが、 C++言語機能として継承を導入する目的または当初の意図ではありませんでした 。これが、アンドレイの答えが受け入れられたままであり続ける理由です。
組合の目的はかなり明白ですが、何らかの理由で人々はそれを非常に頻繁に見逃しています。
結合の目的は、メモリを節約することです異なるメモリオブジェクトを異なる時間に格納するために同じメモリ領域を使用することです それでおしまい。
ホテルの部屋のようなものです。別の人が重複しない期間住んでいます。これらの人々は会うことはなく、一般的にお互いについて何も知りません。部屋のタイムシェアリングを適切に管理することにより(つまり、異なる人が1つの部屋に同時に割り当てられないようにすることにより)、比較的小さなホテルは比較的多くの人に宿泊施設を提供できます。のためです。
それはまさに労働組合が行うことです。プログラム内の複数のオブジェクトが重複しないvalue-lifetimeを持つ値を保持していることがわかっている場合、これらのオブジェクトをユニオンに「マージ」してメモリを節約できます。ホテルの部屋が「アクティブ」なテナントを各瞬間に最大1つ持つように、組合も「アクティブな」メンバーをプログラム時間の各瞬間に最大1つ持っています。 「アクティブな」メンバーのみが読み取り可能です。他のメンバーに書き込むことにより、「アクティブ」ステータスを他のメンバーに切り替えます。
何らかの理由で、この組合の本来の目的は、組合のメンバーの1人を作成し、別のメンバーを介して検査するというまったく異なるもので「オーバーライド」されました。この種の記憶の再解釈(別名「型のunning」)は 組合の有効な使用ではありません。通常、未定義の動作につながります C89/90で実装定義の動作を生成すると説明されています。
EDIT:タイプpunningの目的でのユニオンの使用(つまり、あるメンバーを書き込み、次に別のメンバーを読み取る)には、技術的正誤表のいずれかでより詳細な定義が与えられましたC99標準( DR#257 および DR#28 を参照)ただし、正式にはこれはトラップ表現を読み取ろうとすることで未定義の動作が発生することを防ぐものではないことに注意してください。
ユニオンを使用して、次のような構造体を作成できます。これには、ユニオンのどのコンポーネントが実際に使用されているかを示すフィールドが含まれています。
struct VAROBJECT
{
enum o_t { Int, Double, String } objectType;
union
{
int intValue;
double dblValue;
char *strValue;
} value;
} object;
動作は、言語の観点からは未定義です。プラットフォームごとに、メモリのアライメントとエンディアンの制約が異なる可能性があることを考慮してください。ビッグエンディアンとリトルエンディアンのマシンのコードは、構造体の値を異なる方法で更新します。この言語の動作を修正するには、すべての実装で同じエンディアンネス(およびメモリアライメント制約など)を使用して使用を制限する必要があります。
C++を使用している(2つのタグを使用している)場合、移植性が本当に重要な場合は、構造体を使用して、uint32_t
を取り、ビットマスク操作でフィールドを適切に設定するセッターを提供できます。 Cでも関数を使用して同じことができます。
編集:私は、AProgrammerが投票のために回答を書き留めて、これを閉じることを期待していました。いくつかのコメントが指摘しているように、エンディアンネスは標準の他の部分で各実装に何をすべきかを決定させることによって扱われ、アライメントとパディングも異なる方法で処理できます。ここで、AProgrammerが暗黙的に参照する厳密なエイリアスルールは、ここで重要なポイントです。コンパイラーは、変数の変更(または変更の欠如)を想定できます。ユニオンの場合、コンパイラは命令を並べ替えて、各カラーコンポーネントの読み取りをカラー変数への書き込みに移動できます。
union
の最も一般的なcommonの使用法は、aliasingです。
以下を考慮してください。
union Vector3f
{
struct{ float x,y,z ; } ;
float elts[3];
}
これは何をしますか? eitherの名前でVector3f vec;
のメンバーにきれいできれいにアクセスできます:
vec.x=vec.y=vec.z=1.f ;
または配列への整数アクセスによって
for( int i = 0 ; i < 3 ; i++ )
vec.elts[i]=1.f;
場合によっては、名前でアクセスすることができる最も明確なことです。他の場合、特に軸がプログラムで選択されている場合、より簡単なことは数値インデックスで軸にアクセスすることです-xは0、yは1、zは2です。
おっしゃるように、これは多くのプラットフォームで「機能」しますが、これは厳密には定義されていない動作です。ユニオンを使用する本当の理由は、バリアントレコードを作成することです。
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;
メモリが少ない場合、この構造体は、すべてのメンバーを持つ構造体よりも少ないメモリを使用します。
ちなみにCは提供します
typedef struct {
unsigned int mantissa_low:32; //mantissa
unsigned int mantissa_high:20;
unsigned int exponent:11; //exponent
unsigned int sign:1;
} realVal;
ビット値にアクセスします。
C++では、 Boost Variant は、未定義の動作を可能な限り防ぐように設計された安全なバージョンのユニオンを実装します。
そのパフォーマンスはenum + union
コンストラクト(スタックも割り当てられているなど)と同じですが、enum
の代わりにタイプのテンプレートリストを使用します:)
これは厳密には未定義の動作ですが、実際にはほとんどすべてのコンパイラで動作します。これは非常に広く使用されているパラダイムであり、このような場合、自尊心のあるコンパイラは「正しいこと」を行う必要があります。確かに、いくつかのコンパイラーでは壊れたコードを生成する可能性のある型パニングよりも優先されることは確かです。
動作は未定義かもしれませんが、それは単に「標準」がないことを意味します。すべての適切なコンパイラは、パッキングとアライメントを制御するために #pragmas を提供しますが、デフォルトが異なる場合があります。デフォルトは、使用される最適化設定に応じて変化します。
また、ユニオンはスペースを節約するためにjustではありません。型のパニングを使用して、最新のコンパイラを支援できます。 reinterpret_cast<>
すべての場合、コンパイラは自分が何をしているかを推測できません。それはあなたのタイプについて知っていることを捨てて、もう一度始めなければならないかもしれません(メモリへのライトバックを強制します。最近ではCPUクロック速度に比べて非常に非効率的です)。
他の人は、アーキテクチャの違いについて言及しています(リトル-ビッグエンディアン)。
変数のメモリが共有されているため、1つに書き込むことで他の変数が変化し、その型によっては値が無意味になる可能性があるという問題を読みました。
例えば。 union {float f; int i; } バツ;
X.fから読み取った場合、x.iへの書き込みは意味がありません。フロートの符号、指数、または仮数部を見るために意図したものでない限り。
アライメントの問題もあると思います:一部の変数をWordでアライメントする必要がある場合、期待した結果が得られない可能性があります。
例えば。 union {char c [4]; int i; } バツ;
仮に、あるマシンでcharをWordに揃える必要がある場合、c [0]とc [1]はiとストレージを共有しますが、c [2]とc [3]は共有しません。
ユニオンの実際の使用のもう1つの例として、CORBAフレームワークはタグ付きユニオンアプローチを使用してオブジェクトをシリアル化します。すべてのユーザー定義クラスは1つの(巨大な)ユニオンのメンバーであり、 整数識別子 はデマーシャラーにユニオンの解釈方法を指示します。
技術的には定義されていませんが、実際には、ほとんどの(すべて?)コンパイラーは、あるタイプから別のタイプにreinterpret_cast
を使用するのとまったく同じように扱います。私はあなたの現在のコードで眠りを失うことはありません。
1974年に文書化されたC言語では、すべての構造体メンバーが共通の名前空間を共有し、「ptr-> member」の意味はdefinedでした。メンバーの変位を「ptr」に追加し、メンバーのタイプを使用した結果のアドレス。この設計により、異なる構造定義から取得したメンバー名で同じオフセットを使用して、同じptrを使用することが可能になりました。プログラマーは、その能力をさまざまな目的に使用していました。
構造体メンバーに独自の名前空間が割り当てられると、同じ変位を持つ2つの構造体メンバーを宣言することができなくなりました。言語にユニオンを追加すると、以前のバージョンの言語で使用可能であったのと同じセマンティクスを実現できました(ただし、名前を囲んでいるコンテキストにエクスポートできないため、find-replaceを使用してfoo-> memberを置換する必要がある場合があります) foo-> type1.memberへ)。重要なのは、ユニオンを追加した人が特定のターゲット使用法を念頭に置いているということではなく、むしろ以前のセマンティクスに依存していたプログラマーがどんな目的でも、異なる構文を使用しなければならない場合でも、同じセマンティクスを達成できるはずです。
次の2つの主な理由から、useunionを使用できます。
1ターゲットシステムのメモリアーキテクチャがどのように機能するかを知っていることに基づいて、実際にショートカットコードを記述するCスタイルのハックのようなものです。既に述べたように、実際に多くの異なるプラットフォームをターゲットにしない場合、通常はそれを回避できます。一部のコンパイラでは、パッキングディレクティブも使用できると考えられます(構造体で使用することは知っています)。
2の良い例は、COMで広く使用されている VARIANT タイプにあります。
他の人が言及したように、列挙体と結合され、構造体にラップされたユニオンを使用して、タグ付きユニオンを実装できます。 1つの実用的な使用法は、純粋にenum
を使用して実装されたRustのResult<T, E>
を実装することです(Rustは列挙型に追加データを保持できます)。 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;
}