構造体にインデックスを付けることは合法ですか?


104

コードがどれほど「悪い」かに関わらず、また、アライメントなどがコンパイラー/プラットフォームの問題ではないと想定すると、これは未定義または壊れた動作ですか?

私がこのような構造体を持っている場合:-

struct data
{
    int a, b, c;
};

struct data thing;

それは法的にアクセスするabcのように(&thing.a)[0](&thing.a)[1](&thing.a)[2]

いずれの場合も、すべてのコンパイラとプラットフォームで試してみましたが、すべての設定で「機能しました」。コンパイラがbthing [1]が同じものであることに気付かず、 'b'へのストアがレジスターに入れられ、thing(1)がメモリーから誤った値を読み取る(たとえば)のではないかと心配しています。すべてのケースで私はそれを試したが、正しいことをした。(もちろん、それはあまり証明されていません)

これは私のコードではありません。それは私が作業しなければならないコードです、これは悪いコードであるか壊れたコードであるかに興味があります。

タグ付きCおよびC ++。私は主にC ++に興味がありますが、それが異なる場合はCも興味があります。


51
いいえ、「合法」ではありません。これは未定義の動作です。
Sam Varshavchik 2016年

10
コンパイラーがメンバー間にパディングを追加しないため、この非常に単純なケースで機能します。さまざまなサイズのタイプを使用して構造を試してみてください。クラッシュしてしまいます。
一部のプログラマー、

7
過去を掘り下げる-UBはニックネームの付いた鼻のデーモンでした。
エイドリアン・コロミッチ

21
すばらしいですね。Cタグをたどり、質問を読んだ後、Cにのみ当てはまる回答を書きました。C++タグが見つからなかったためです。ここではCとC ++は大きく異なります!Cでは共用体による型パンニングを許可していますが、C ++ではできません。
Lundin

7
要素として配列にアクセスする必要がある場合は、配列として定義します。別の名前にする必要がある場合は、名前を使用します。ケーキを作って食べようとすると、最終的には消化不良につながります—おそらく、想像できない最も不便な時期に。(Cではインデックス0は有効です。インデックス1または2は無効です。単一の要素がサイズ1の配列として扱われるコンテキストがあります。)
Jonathan Leffler

回答:


73

違法です1。これはC ++での未定義の動作です。

あなたは配列の方法でメンバーを取っていますが、これはC ++標準が言うことです(私の強調):

[dcl.array / 1] ...配列タイプのオブジェクトには、タイプTのN個のサブオブジェクトの連続して割り当てられた空でないセットが含まれています...

ただし、メンバーには、このような連続した要件はありません。

[class.mem / 17] ...;実装のアライメント要件により、2つの隣接するメンバーがお互いの直後に割り当てられない場合がある ...

上記の2つの引用符はstruct、C ++標準で定義された動作ではないようにaにインデックスを付ける理由を示すのに十分なはずですが、1つの例を挙げましょう。式を見てください(&thing.a)[2]-添字演算子について:

[expr.post//expr.sub/1] 角かっこで囲まれた式が後に続く後置式は後置式です。式の1つは、「Tの配列」タイプのglvalueまたは「Tへのポインター」タイプのprvalueである必要があり、もう一方は、スコープなし列挙型または整数型のprvalueである必要があります。結果はタイプ「T」です。タイプ「T」は完全に定義されたオブジェクトタイプです。66 E1[E2]は(定義により)と同一です。((E1)+(E2))

上記の引用の太字のテキストを掘り下げます:ポインター型への整数型の追加に関して(ここでは強調に注意してください)。

[expr.add / 4]整数型の式がポインターに加算またはポインターから減算されると、結果はポインターオペランドの型になります。式 がn個の要素を持つ配列オブジェクトの要素を指す場合、式and(whereは値を持っています)は(おそらく仮説的な)要素を指します if; それ以外の場合、動作は未定義です。...Px[i]xP + JJ + PJjx[i + j]0 ≤ i + j ≤ n

if句の配列要件に注意してください。それ以外の場合は、上記の引用に含まれています。この式は明らかにif句の対象ではありません。したがって、未定義の動作。(&thing.a)[2]


余談ですが、私はさまざまなコンパイラでコードとそのバリエーションを広範囲にわたって実験しましたが、ここではパディングを導入していません(動作します)。メンテナンスの観点から、コードは非常に壊れやすいです。これを行う前に、実装がメンバーを連続的に割り当てたことをまだ主張する必要があります。そして、インバウンドにとどまる:-)。しかし、まだ未定義の動作....

(定義された動作を伴う)いくつかの実行可能な回避策は、他の回答によって提供されています。



コメントで正しく指摘されているように、以前の編集であった[basic.lval / 8]は適用されません。@ 2501と@MMに感謝

1thing.aこのパートを介して構造体のメンバーにアクセスできる唯一の法的ケースについては、この質問に対する@Barry の回答を参照してください。


1
@jcoderこれはclass.memで定義されています。実際のテキストについては、最後の段落を参照してください。
NathanOliver 2016年

4
厳密なアライシングはここでは関係ありません。型intは集約型に含まれており、この型はintのエイリアスになる場合があります。- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501

1
@反対投票者、コメントしてもいいですか?-そして、この答えが間違っているところを改善または指摘するには?
WhiZTiM 2016年

4
厳密なエイリアシングはこれとは無関係です。パディングは、オブジェクトの格納された値の一部ではありません。また、この回答は、最も一般的なケースであるパディングがない場合に何が起こるかについては対処できません。この回答を実際に削除することをお勧めします。
MM

1
できた!厳格なエイリアスに関する段落を削除しました。
WhiZTiM 2016年

48

いいえ。Cでは、パディングがない場合でも、これは未定義の動作です。

未定義の動作を引き起こすものは、範囲外のアクセス1です。スカラー(構造体のメンバーa、b、c)があり、それを配列2として使用して次の仮想要素にアクセスしようとすると、同じタイプの別のオブジェクトが偶然発生しても、未定義の動作が発生します。そのアドレス。

ただし、構造体オブジェクトのアドレスを使用して、特定のメンバーへのオフセットを計算できます。

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

これは各メンバーに対して個別に行う必要がありますが、配列アクセスに似た関数に入れることができます。


1(引用:ISO / IEC 9899:201x 6.5.6加算演算子8)
結果が配列オブジェクトの最後の要素の1つ後を指している場合、それは評価される単項*演算子のオペランドとして使用されません。

2(引用:ISO / IEC 9899:201x 6.5.6加算演算子7)
これらの演算子の目的では、配列の要素ではないオブジェクトへのポインターは、配列の最初の要素へのポインターと同じように動作しますエレメントタイプとしてオブジェクトのタイプを持つ長さ1の配列。


3
これは、クラスが標準のレイアウトタイプである場合にのみ機能することに注意してください。そうでない場合は、まだUBです。
NathanOliver 2016年

@NathanOliver私の回答はCのみに適用されることを述べておかなければなりません。これは、このような二重タグ言語の質問の問題の1つです。
2501 2016年

おかげで、違いを知るのは興味深いので、C ++とCを別々に尋ねたのはそのためです
jcoder

@NathanOliver標準レイアウトの場合、最初のメンバーのアドレスはC ++クラスのアドレスと一致することが保証されています。ただし、アクセスが明確に定義されていることを保証するものでも、他のクラスでのそのようなアクセスが未定義であることを意味するものでもありません。
Potatoswatter 2016年

それchar* p = ( char* )&thing.a + offsetof( thing , b );は未定義の行動につながると思いますか?
MM

43

C ++では、本当に必要な場合は、operator []を作成します。

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

機能することが保証されているだけでなく、使用方法も簡単です。判読できない式を記述する必要はありません (&thing.a)[0]

注:この回答は、フィールドを持つ構造がすでにあり、インデックスを介してアクセスを追加する必要があることを前提としています。速度が問題で、構造を変更できる場合、これはより効果的です。

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

このソリューションは構造のサイズを変更するため、メソッドも使用できます。

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

1
タイプのパンニングを使用したCプログラムの逆アセンブルと比較して、この逆アセンブルを確認してください。しかし、しかし... C ++はCと同じくらい高速です...そうですか?正しい?
ランディン

6
@Lundinこの構築の速度を気にする場合は、データを個別のフィールドとしてではなく、最初に配列として編成する必要があります。
スラバ

2
@Lundinは、どちらも読み取り不能および未定義の動作を意味しますか?結構です。
スラバ

1
@Lundin演算子のオーバーロードは、コンパイル時の構文機能であり、通常の関数と比較してオーバーヘッドを引き起こしません。見てみましょうgodbolt.org/g/vqhREzそれはC ++とCコードをコンパイルするとき、コンパイラが実際に何を見るために。彼らが何をしていて、何を期待しているのかは驚くべきことです。私は個人的には、Cの100万回よりもC ++の型安全性と表現力の向上を好みます。そして、パディングに関する仮定に依存することなく、常に機能します。
イエンス

2
これらの参照は、少なくともサイズを2倍にします。ただしてくださいthing.a()
2016年

14

c ++の場合:名前を知らなくてもメンバーにアクセスする必要がある場合は、メンバー変数へのポインターを使用できます。

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

1
これは言語機能を使用しているため、明確に定義されており、私が想定しているように効率的です。ベストアンサー。
ピーター-モニカの復活2016年

2
効率的ですか?私は反対だと思います。生成されたコードを見てください
JDługosz

1
@JDługosz、あなたはまったく正しいです。PEEK撮るアセンブリ生成時に、GCC 6.2が使用するコードと同等を作成するようだoffsetoffC.で
Unslanderモニカ-落語

3
また、arr constexprを作成して改善することもできます。これは、オンザフライで作成するのではなく、データセクションに単一の固定ルックアップテーブルを作成します。
Tim

10

ISO C99 / C11では、ユニオンベースの型パンニングが有効であるため、非配列へのポインターにインデックスを付ける代わりにそれを使用できます(他のさまざまな回答を参照してください)。

ISO C ++では、ユニオンベースの型パンニングは許可されていません。 GNU C ++は拡張機能として機能しますが、一般的にGNU拡張機能をサポートしていない他の一部のコンパイラーは、共用体型パンニングをサポートしています。しかし、これは厳密に移植可能なコードを書くのに役立ちません。

gccとclangの現在のバージョンではswitch(idx)、メンバーを選択するためにを使用してC ++メンバー関数を作成すると、コンパイル時の定数インデックスが最適化されますが、ランタイムインデックスにひどいブランチアセンブリが生成されます。これには本質的に問題はありませんswitch()。これは単に、現在のコンパイラーで最適化に失敗したバグです。彼らはSlavaのswitch()関数を効率的にコンパイルできます。


これに対する解決策/回避策は、それを別の方法で行うことです。クラス/構造体に配列メンバーを与え、特定の要素に名前を付けるアクセサ関数を記述します。

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

Godboltコンパイラーエクスプローラーで、さまざまなユースケースのasm出力を確認できます。これらは完全なx86-64 System V関数であり、インライン化したときに得られる結果をよりよく示すために、末尾のRET命令が省略されています。ARM / MIPS /何でも同様です。

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

対照的に、switch()C ++のforS を使用した@Slavaの回答では、ランタイム変数インデックスに対してasmはこのようになります。(前のGodboltリンクのコード)。

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

これは、C(またはGNU C ++)のユニオンベースの型パンニングバージョンと比較して、明らかにひどいです。

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

@MM:良い点。これは、さまざまなコメントに対する回答であり、スラバの回答に対する代替案です。冒頭の言葉を書き直したので、少なくとも最初の質問への回答として始めました。ご指摘いただきありがとうございます。
Peter Cordes

使用しながら組合ベース型パンニングもgccと打ち鳴らすに動作するようながら[]、組合員に直接標準定義をオペレータのarray[index]と等価であるとして*((array)+(index))、およびGCCも打ち鳴らすも確実にアクセスがあることを認識するであろう*((someUnion.array)+(index))へのアクセスですsomeUnion。私が見ることができる唯一の説明は、ということであるsomeUnion.array[index]にも*((someUnion.array)+(index))標準で定義されますが、単に人気の拡張であり、とgcc /打ち鳴らすされていない、少なくとも今のところ、二をサポートしますが、最初のをサポートするように見えるしないように選択しています。
スーパーキャット

9

C ++では、これはほとんど未定義の動作です(どのインデックスに依存します)。

[expr.unary.op]から:

ポインター演算(5.7)および比較(5.9、5.10)の目的で、この方法でアドレスが取得される配列要素ではないオブジェクトは、タイプが1つの要素の配列に属すると見なされTます。

したがって、式&thing.aは1の配列を参照すると見なされますint

[expr.sub]から:

E1[E2]は(定義により)と同一です。*((E1)+(E2))

そして[expr.add]から:

整数型の式がポインターに加算またはポインターから減算されると、結果はポインターオペランドの型になります。発現ならP要素を指しx[i]配列オブジェクトのx持つn要素、式P + J及びJ + PJ値が持つj(おそらく、仮想的な)要素への)点がx[i + j]あれば0 <= i + j <= n、それ以外の場合、動作は未定義です。

(&thing.a)[0]&thing.aサイズ1の配列と見なされ、最初のインデックスを取得するため、完全に整形式です。これは、許可されるインデックスです。

(&thing.a)[2]前提条件に違反している0 <= i + j <= n、我々は持っているのでi == 0j == 2n == 1。単にポインタを作成すること&thing.a + 2は未定義の動作です。

(&thing.a)[1]興味深いケースです。[expr.add]の実際の違反にはなりません。配列の終わりを過ぎたところにポインタを置くことができます-これはそうなります。ここで、[basic.compound]のメモを見てみましょう。

オブジェクトの末尾へのポインタまたはオブジェクトの末尾を超えるポインタ型の値は、object53が占有するメモリの最初のバイト(1.7)のアドレス、またはオブジェクトが占有するストレージの終了後のメモリの最初のバイトを表します、それぞれ。[注:オブジェクトの最後(5.7)を過ぎたポインターは、そのアドレスにある可能性のある、オブジェクトのタイプの無関係なオブジェクトを指しているとは見なされません。

したがって、ポインターの取得は&thing.a + 1動作の定義ですが、何も指していないため、ポインターの逆参照は未定義です。


(&thing.a)+ 1の評価は、配列の終わりを過ぎたポインタが正当であるため、ほぼ正当です。&thing.bと<、>、<=、> =を比較すると、そこに格納されているデータの読み取りまたは書き込みは未定義の動作です。(&thing.a)+ 2は絶対に違法です。
gnasher729 2016年

@ gnasher729ええ、それは答えをもう少し明確にする価値があります。
バリー

これ(&thing.a + 1)は私がカバーできなかった興味深いケースです。+1!...気になりますが、ISO C ++委員会に参加していますか?
WhiZTiM 2016年

それ以外の場合、ポインターをハーフオープン間隔として使用するすべてのループがUBになるため、これも非常に重要なケースです。
Jens

最後の標準引用について。ここではC ++をCよりも適切に指定する必要があります。
2501

8

これは未定義の動作です。

C ++には多くのルールがあり、コンパイラにあなたが何をしているのかを理解する希望を与えようとするので、それについて推論して最適化することができます。

エイリアシング(2つの異なるポインター型を介してデータにアクセスする)、配列の境界などに関する規則があります。

変数がある場合x、それが配列のメンバーではないという事実は、コンパイラーが、[]ベースの配列アクセスでは変数を変更できないと想定できることを意味します。したがって、使用するたびにメモリからデータを常にリロードする必要はありません。誰かがその名前から変更できた場合のみ。

したがって(&thing.a)[1]、コンパイラは、を参照しないと想定できますthing.b。この事実を使用して、への読み取りと書き込みを並べ替え、実際にthing.b実行するように指示したことを無効にすることなく、実行したいことを無効にすることができます。

この典型的な例は、constのキャストです。

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

ここでは、通常、コンパイラーに7、2、!= 7、2つの同一のポインターと表示されます。それptrが指しているという事実にもかかわらずx。コンパイラxは、定数の値であるという事実を利用して、の値を要求したときにわざわざそれを読み取らないようにしますx

しかし、のアドレスを取得するとx、それを強制的に存在させます。次に、constをキャストして変更します。したがって、メモリ内の実際の場所xが変更されているため、コンパイラは読み取り時に実際にそれを読み取らないようにできますx

コンパイラーは、をフォローptrすることを避ける方法さえ理解できるほど賢くなります*ptrが、多くの場合そうではありません。ptr = ptr+argc-1オプティマイザがあなたより賢くなっている場合は、遠慮なく使用してください。

operator[]適切なアイテムを取得するカスタムを提供できます。

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

両方があると便利です。


「それが配列のメンバーではないという事実は、コンパイラーが[]ベースの配列アクセスで変更できないと想定できることを意味します。」-真実ではない、たとえば(&thing.a)[0]変更される可能性がある
MM

constの例が質問とどのように関係しているかはわかりません。これが失敗するのは、constオブジェクトが変更されないという特定のルールがあり、それ以外の理由がないためです。
MM

1
@MMは、それが構造体の中にインデクシングの例はありませんが、それはだ非常にそのにより基準何かに未定義の動作を使用する方法の良い例見かけコンパイラができるので、予想以上のメモリ内の場所、異なる出力をもたらすことが何かを行うとあなたがそれを望んだよりもUB。
ワイルドカード2016年

@MM申し訳ありませんが、オブジェクト自体へのポインタを介した簡単なもの以外の配列アクセスはありません。そして2番目のものは、未定義の動作の副作用を簡単に確認できる例にすぎません。コンパイラは、定義された方法で変更できないxことがわかっいるため、読み取りを最適化します。同様の最適化は、コンパイラが変更可能なアクセスが定義されていないことを証明できる場合にbvia を変更すると発生する(&blah.a)[1]可能性がありbます。このような変更は、コンパイラ、周囲のコードなど、一見無害な変更が原因で発生する可能性があります。したがって、それが機能することをテストするだけでは十分ではありません。
Yakk-Adam Nevraumont 2016年

6

プロキシクラスを使用して、メンバー配列の要素に名前でアクセスする方法を次に示します。これは非常にC ++であり、構文の設定を除いて、refを返すアクセサー関数と比較して利点はありません。これは、->メンバーとして要素にアクセスするためにオペレーターをオーバーロードするため、受け入れられるようにするには、アクセサー(d.a() = 5;)の構文を嫌うだけでなく->、非ポインターオブジェクトでの使用を許容する必要があります。これはまた、コードに精通していない読者を混乱させるかもしれないと思うので、これはあなたがプロダクションに入れたいものよりも巧妙なトリックかもしれません。

Dataこのコードの構造体には、添字演算子のオーバーロードも含まれています。これにより、ar配列メンバー内のインデックス付き要素にアクセスしbeginたり、end関数や関数に繰り返しアクセスしたりできます。また、これらすべてが非constおよびconstバージョンでオーバーロードされているため、完全にするために含める必要があると感じました。

ときDataさんは->(次のように:名前によって要素にアクセスするために使用されるmy_data->b = 5;)、Proxyオブジェクトが返されます。次に、このProxy右辺値はポインタではないため、それ自体の->演算子は自動チェーン呼び出しされ、それ自体へのポインタを返します。このようにして、Proxyオブジェクトはインスタンス化され、初期式の評価中も有効のままです。

Contruction Proxy目的は、その3つの基準部材を取り込みabそしてc型テンプレートパラメータとして与えられる少なくとも3つの値を含むバッファを指しているものとするコンストラクタに渡されたポインタに記載の方法T。したがって、Dataクラスのメンバーである名前付き参照を使用する代わりに、アクセスポイントで参照を生成することでメモリを節約します(ただし、残念ながら、演算子->ではなく使用し.ます)。

コンパイラのオプティマイザがの使用によって導入されたすべての間接参照をどれだけうまく排除するかをテストするためにProxy、以下のコードには2つのバージョンのが含まれていますmain()#if 1バージョンは、使用->及び[]オペレータ、および#if 0バージョンがのみ直接アクセスすることにより、手順の等価なセットを実行しますData::ar

このNci()関数は、配列要素を初期化するための実行時整数値を生成します。これにより、オプティマイザが定数値を各std::cout <<呼び出しに直接接続するだけで済みません。

gcc 6.2の場合、-O3を使用すると、の両方のバージョンでmain()同じアセンブリが生成されます(最初と比較する前#if 1#if 0前を切り替えてmain()):https : //godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

気の利いた。主にこれが最適化されることを証明したため賛成です。ところで、全体main()をタイミング関数で書くのではなく、非常に単純な関数を書くことで、はるかに簡単にそれを行うことができます!たとえば、/ (godbolt.org/g/89d3Npint getb(Data *d) { return (*d)->b; }だけにコンパイルされます。(はい、構文が簡単になりますが、refの代わりにポインターを使用して、この方法でオーバーロードする奇妙さを強調しています。)mov eax, DWORD PTR [rdi+4]retData &d->
Peter Cordes

とにかく、これはクールです。のような他のアイデアint tmp[] = { a, b, c}; return tmp[idx];はすぐに最適化されないので、これがうまくいくのは素晴らしいことです。
Peter Cordes

operator.C ++ 17で見逃しているもう1つの理由。
イェンス

2

値を読み取るだけで十分で、効率が問題にならない場合、またはコンパイラーが適切に最適化すると信頼している場合、またはstructがその3バイトだけの場合は、安全にこれを行うことができます。

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

C ++のみのバージョンの場合、これを使用static_assertstruct dataて標準レイアウトがあることを確認し、おそらく無効なインデックスで例外をスローすることをお勧めします。


1

これは違法ですが、回避策があります:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

これでvにインデックスを付けることができます:


6
多くのC ++プロジェクトは、あらゆる場所でダウンキャストすることは問題ないと考えています。私たちはまだ悪い習慣を説くべきではありません。
StoryTeller-Unslander Monica 2016年

2
ユニオンは、両方の言語の厳密なエイリアシングの問題を解決します。しかし、共用体を介した型パンニングは、C ++ではなくCでのみ問題ありません。
Lundin

1
それでも、これがすべてのc ++コンパイラーの100%で機能しても、驚かないでしょう。今まで。
Sven Nilsson、

1
最も積極的なオプティマイザ設定をオンにして、gccでそれを試すことができます。
ランディン

1
@Lundin:ISO C ++の拡張として、ユニオン型のパンニングはGNU C ++では合法です。マニュアルにはあまり明記されていないようですが、私はかなり確信しています。それでも、この回答は、それが有効な場所と無効な場所を説明する必要があります。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.