web-dev-qa-db-ja.com

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

コードがどれだけ「悪い」か、およびアライメントなどがコンパイラ/プラットフォームの問題ではないと仮定すると、これは未定義または壊れた動作ですか?

このような構造体がある場合:-

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

struct data thing;

ab、およびc(&thing.a)[0](&thing.a)[1]、および(&thing.a)[2]としてアクセスするにはlegalですか?

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

これは私のコードではありません。それは私が作業しなければならないコードです、私はこれがbadコードかbrokenコードかどうかに興味があります

タグ付きのCおよびC++。私は主にC++に興味がありますが、興味がある場合はCも違います。

102
jcoder

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

配列形式でメンバーを取得していますが、C++標準で言うことは次のとおりです(強調)。

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

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

[class.mem/17] ...;実装アライメント要件2つの隣接するメンバーがすぐに割り当てられない可能性があります。 ..

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

[expr.post//expr.sub/1] :角括弧内の式が後に続く後置式は後置式です。式の1つは、「Tの配列」型のglvalueまたは「Tへのポインター」型のprvalueであり、もう1つの式は、有効範囲なし列挙または整数型のprvalueでなければなりません。結果のタイプは「T」です。型「T」は完全に定義されたオブジェクト型でなければなりません。66E1[E2]は(定義により)((E1)+(E2))と同一です。

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

[expr.add/4]整数型の式がポインターに加算またはポインターから減算される場合、結果はポインターオペランドの型になります。 IfPが要素x[i] of an arrayobject x要素がn個の場合、式P + JおよびJ + P(ここでJの値はj)は(おそらく仮説の)要素x[i + j] if 0 ≤ i + j ≤ n;を指します。 その他、動作は未定義です。 ...

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


補足説明:さまざまなコンパイラでコードとそのバリエーションを広範囲に実験しましたが、ここではパディングを導入していませんが、(itworks);メンテナンスの観点から見ると、コードは非常に脆弱です。これを行う前に、実装がメンバーを連続して割り当てていることを引き続きアサートする必要があります。そしてインバウンドにとどまります:-)。しかし、未定義の動作です...

いくつかの実行可能な回避策(定義済みの動作)が他の回答によって提供されています。



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

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

71
WhiZTiM

いいえ。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の配列の最初の要素へのポインターと同じように動作しますタイプ。

49
2501

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]; }
};
43
Slava

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;
14
StoryTeller

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

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

現在のバージョンのgccおよびclangでは、switch(idx)を使用してメンバーを選択するC++メンバー関数を作成すると、コンパイル時の定数インデックスが最適化されますが、実行時インデックスのひどい分岐asmが生成されます。 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 compiler Explorer で、さまざまなユースケースの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]

比較すると、C++でswitch()を使用する@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]
10
Peter Cordes

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

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つの同一のポインターというコンパイラーが表示されます。 ptrxを指しているという事実にもかかわらず。コンパイラは、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;

両方を持つと便利です。

C++では、これはmostly未定義の動作です(どのインデックスに依存するか)。

[expr.unary.op]から:

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

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

[expr.sub]から:

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

[expr.add]から:

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

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

(&thing.a)[2]は、0 <= i + j <= ni == 0j == 2があるため、n == 1という前提条件に違反しています。ポインタ&thing.a + 2を単純に構築することは、未定義の動作です。

(&thing.a)[1]は興味深いケースです。 [expr.add]の実際には何も違反していません。配列の最後から1つ後のポインターを取ることができます-これはそうです。ここで、[basic.compound]のメモに目を向けます。

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

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

8
Barry

プロキシクラスを使用して、名前でメンバー配列の要素にアクセスする方法を次に示します。これは非常にC++であり、構文の優先順位を除き、refを返すアクセサー関数に対して利点はありません。これは->演算子をオーバーロードして要素をメンバーとしてアクセスするため、アクセサの構文(d.a() = 5;)を嫌うだけでなく、->をnonポインターオブジェクト。これはまた、コードに精通していない読者を混乱させるかもしれないので、本番に入れたいものよりも、これはきちんとしたトリックになるかもしれません。

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

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

Proxyオブジェクトの構築により、コンストラクターに渡されるポインターに従って、その3つの参照メンバーab、およびcが生成されます。型がテンプレートパラメータTとして指定されている少なくとも3つの値。したがって、Dataクラスのメンバーである名前付き参照を使用する代わりに、アクセスポイントで参照を作成することでメモリを節約します(ただし、残念ながら、->演算子ではなく.演算子を使用します) 。

コンパイラーのオプティマイザーがProxyの使用によって導入されたすべての間接性をどの程度除去するかをテストするために、以下のコードにはmain()の2つのバージョンが含まれています。 #if 1バージョンは->および[]演算子を使用し、#if 0バージョンはData::arに直接アクセスすることによってのみ、同等の手順を実行します。

Nci()関数は、配列要素を初期化するためのランタイム整数値を生成し、オプティマイザーが各std::cout<<呼び出しに定数値を直接プラグインすることを防ぎます。

Gcc 6.2では、-O3を使用して、main()の両方のバージョンが同じアセンブリを生成します(比較する最初のmain()の前に#if 1#if 0を切り替えます): 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
6

値の読み取りが十分で、効率が問題にならない場合、またはコンパイラが物事を最適化することを信頼している場合、または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_assertを使用してstruct dataが標準レイアウトであることを確認し、代わりに無効なインデックスで例外をスローすることをお勧めします。

2
hyde

違法ですが、回避策があります。

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

これで、vのインデックスを作成できます。

1
Sven Nilsson