web-dev-qa-db-ja.com

構造体では、ある配列フィールドを使用して別の配列フィールドにアクセスすることは合法ですか?

例として、次の構造を考えます。

struct S {
  int a[4];
  int b[4];
} s;

s.a[6]を記述し、それがs.b[2]と等しいと期待することは合法ですか?個人的には、C++ではUBでなければならないと感じていますが、Cについてはわかりません。しかし、CおよびC++言語の標準に関連するものを見つけることができませんでした。


更新

コードを確実に機能させるために、フィールド間にパディングがないことを確認する方法を提案するいくつかの回答があります。そのようなコードがUBである場合、パディングの不在は十分ではないことを強調したいと思います。 UBの場合、コンパイラはS.a[i]S.b[j]へのアクセスが重複しないと見なし、そのようなメモリアクセスの順序を自由に変更できます。例えば、

    int x = s.b[2];
    s.a[6] = 2;
    return x;

に変換することができます

    s.a[6] = 2;
    int x = s.b[2];
    return x;

常に2を返します。

51
Nikolai

S.a [6]を記述し、それがs.b [2]と等しいと期待することは合法ですか?

いいえ。 CおよびC++では、範囲外の配列にアクセスすると呼び出されるundefined behaviourであるためです。

C11 J.2未定義の動作

  • 配列オブジェクトと整数型への、またはそれを超えたポインターの加算または減算は、配列オブジェクトのすぐ上を指す結果を生成し、評価される単項*演算子のオペランドとして使用されます(6.5。 6)。

  • オブジェクトが指定された添え字で明らかにアクセス可能である場合でも、配列添え字は範囲外です(宣言int a[1][7](6.5.6)で与えられた左辺値式a[4][5])のように)。

C++標準 ドラフト セクション5.7加法演算子のパラグラフ5は次のように述べています:

整数型を持つ式がポインターに加算またはポインターから減算される場合、結果はポインターオペランドの型になります。ポインターオペランドが配列オブジェクトの要素を指し、配列が十分に大きい場合、結果は、結果の配列要素と元の配列要素の添字の差が整数式と等しくなるように、元の要素からオフセットした要素を指します。 [...]ポインタオペランドと結果の両方が同じ配列オブジェクトの要素、または配列オブジェクトの最後の要素の1つを指す場合、評価はオーバーフローを生成しません。それ以外の場合、動作は未定義です。

61
msc

@rspUndefined behavior for an array subscript that is out of range)の答えとは別に、bを介してaにアクセスすることは違法であると付け加えることができます。 aに割り当てられた領域の終わりとbの始まりの間にあるため、特定の実装で実行できる場合でも、移植性はありません。

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+

struct objectの配置はaの配置と同じですが、bの配置と同じですが、C言語も2番目のパディングを強制しません。そこにいる。

34
alinsoar

abは2つの異なる配列であり、aは_4_要素を含むと定義されます。したがって、_a[6]_は範囲外の配列にアクセスするため、未定義の動作になります。配列の添字_a[6]_は*(a+6)として定義されているため、UBの証明は、実際には、ポインターと組み合わせたセクション「加算演算子」によって与えられます。C11標準(例: this オンラインドラフトバージョン)

6.5.6加算演算子

整数型の式がポインターに加算またはポインターから減算される場合、結果にはポインターオペランドの型が含まれます。ポインターオペランドが配列オブジェクトの要素を指し、配列が十分に大きい場合、結果は、結果の配列要素と元の配列要素の添字の差が整数式と等しくなるように、元の要素からオフセットした要素を指します。つまり、式Pが配列オブジェクトのi番目の要素を指す場合、式(P)+ N(同等にN +(P))および(P)-N(Nは値n)を指します存在する場合、それぞれ、配列オブジェクトのi + n番目とin番目の要素に。さらに、式Pが配列オブジェクトの最後の要素を指す場合、式(P)+1は配列オブジェクトの最後の要素を1つ指し、式Qが配列オブジェクトの最後の要素を1つ指す場合、式(Q)-1は、配列オブジェクトの最後の要素を指します。 ポインタオペランドと結果の両方が同じ配列オブジェクトの要素、または配列オブジェクトの最後の要素の1つを指す場合、評価はオーバーフローを生成しません。それ以外の場合、動作は未定義です。結果が配列オブジェクトの最後の要素の1つ先を指す場合、評価される単項*演算子のオペランドとして使用されません。

同じ引数がC++にも適用されます(ただし、ここでは引用しません)。

さらに、aの配列境界を超えるという事実のため、明らかに未定義の動作ですが、コンパイラがメンバーabの間にパディングを導入する可能性があることに注意してください。そのようなポインター演算が許可されていたとしても-_a+6_は必ずしも_b+2_と同じアドレスを生成するとは限りません。

11
Stephan Lechner

合法ですか?いいえ。他の人が述べたように、未定義の動作を呼び出します。

機能しますか?それはコンパイラに依存します。それが未定義の動作に関することです:ndefinedです。

多くのCおよびC++コンパイラでは、構造体は、bがメモリ内でaの直後に続き、境界チェックが行われないようにレイアウトされます。そのため、a [6]へのアクセスは実質的にb [2]と同じになり、いかなる種類の例外も発生しません。

与えられた

struct S {
  int a[4];
  int b[4];
} s

および余分なパディングがないと仮定の場合、この構造は実際には8つの整数を含むメモリブロックを見る方法にすぎません。 (int*)にキャストでき、((int*)s)[6]s.b[2]と同じメモリを指します。

この種の動作に依存する必要がありますか?絶対違う。 未定義は、コンパイラがこれをサポートする必要がないことを意味します。コンパイラは、&(s.b [2])==&(s.a [6])が正しくないという仮定を与える可能性のある構造を自由に埋めることができます。コンパイラーは、配列アクセスに境界チェックを追加することもできます(ただし、コンパイラーの最適化を有効にすると、おそらくそのようなチェックは無効になります)。

私は過去にこの影響を経験しました。このような構造を持つことは非常に一般的です

struct Bob {
    char name[16];
    char whatever[64];
} bob;
strcpy(bob.name, "some name longer than 16 characters");

これで、bob.whateverは「16文字より」になります。 (これが、strncpy、BTWを常に使用する必要がある理由です)

6
dwilliss

@MartinJamesがコメントで言及したように、abが連続したメモリ内にあることを保証する必要がある場合(または少なくともそのように扱うことができる場合(編集)、アーキテクチャ/コンパイラが異常なメモリブロックサイズ/オフセット、およびパディングを追加する必要がある強制アライメント)、unionを使用する必要があります。

_union overlap {
    char all[8]; /* all the bytes in sequence */
    struct { /* (anonymous struct so its members can be accessed directly) */
        char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */
        char b[4];
    };
};
_

bからaに直接アクセスすることはできません(たとえば、要求したように_a[6]_)が、can両方の要素にアクセスしますabを使用してall(たとえば、_all[6]_は_b[2]_と同じメモリ位置を参照します)。

(編集:上記のコードの_8_と_4_をそれぞれ2*sizeof(int)sizeof(int)に置き換えると、アーキテクチャのアライメントに一致する可能性が高くなり、特にコードの移植性を高める必要がある場合は、ab、またはallに含まれるバイト数に関する仮定を避けるように注意する必要があります。ただし、おそらく最も一般的な(1、2、および4バイト)のメモリ配置で動作します。)

以下に簡単な例を示します。

_#include <stdio.h>

union overlap {
    char all[2*sizeof(int)]; /* all the bytes in sequence */
    struct { /* anonymous struct so its members can be accessed directly */
        char a[sizeof(int)]; /* low Word */
        char b[sizeof(int)]; /* high Word */
    };
};

int main()
{
    union overlap testing;
    testing.a[0] = 'a';
    testing.a[1] = 'b';
    testing.a[2] = 'c';
    testing.a[3] = '\0'; /* null terminator */
    testing.b[0] = 'e';
    testing.b[1] = 'f';
    testing.b[2] = 'g';
    testing.b[3] = '\0'; /* null terminator */
    printf("a=%s\n",testing.a); /* output: a=abc */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abc */

    testing.a[3] = 'd'; /* makes printf keep reading past the end of a */
    printf("a=%s\n",testing.a); /* output: a=abcdefg */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abcdefg */

    return 0;
}
_
5
Jed Schaaf

No、CおよびC++の両方で、範囲外の配列にアクセスするとUndefined Behaviorが呼び出されるため。

3
gsamaras

短い答え:いいえ。あなたは未定義の行動の土地にいます。

長い答え:いいえしかし、だからといって、他の大雑把な方法でデータにアクセスできないわけではありません... GCCを使用している場合次のようなことができます(dwillisの答えの詳細):

struct __attribute__((packed,aligned(4))) Bad_Access {
    int arr1[3];
    int arr2[3];
};

そして、Godbolt source + asm )経由でアクセスできました

int x = ((int*)ba_pointer)[4];

しかし、そのキャストは厳密なエイリアスに違反するため、g++ -fno-strict-aliasingでのみ安全です。構造体ポインターを最初のメンバーへのポインターにキャストできますが、最初のメンバーの外部にアクセスしているため、UBボートに戻ります。

あるいは、それをしないでください。将来のプログラマー(おそらくあなた自身)をその混乱の心痛から救いなさい.

また、作業中にstd :: vectorを使用してみませんか?それは絶対確実ではありませんが、バックエンドにはそのような悪い振る舞いを防ぐためのガードがあります。

補遺:

パフォーマンスが本当に心配な場合:

アクセスしている2つの同じ型のポインターがあるとします。コンパイラはおそらく、両方のポインタが干渉する可能性があると想定し、愚かなことからあなたを保護するために追加のロジックをインスタンス化します。

エイリアスを作成しようとしていないことをコンパイラに厳wearに誓うと、コンパイラは気前よくあなたに報酬を与えます: restrictキーワードはgcc/g ++で大きな利点を提供します

結論:悪にならないでください。あなたの将来、とコンパイラーに感謝します。

1
Alex Shirley

Jed Schaffの答えは正しい軌道に乗っていますが、完全に正しいわけではありません。コンパイラがabの間にパディングを挿入した場合、彼のソリューションは依然として失敗します。ただし、宣言する場合:

_typedef struct {
  int a[4];
  int b[4];
} s_t;

typedef union {
  char bytes[sizeof(s_t)];
  s_t s;
} u_t;
_

これで、コンパイラーが構造をどのようにレイアウトするかに関係なく、_(int*)(bytes + offsetof(s_t, b))_にアクセスして_s.b_のアドレスを取得できます。 offsetof()マクロは_<stddef.h>_で宣言されています。

sizeof(s_t)は定数式であり、CおよびC++の両方の配列宣言で有効です。可変長配列は提供しません。 (以前にC標準を誤読していたことをおologiesびします。それは間違っているように思えました。)

ただし、現実の世界では、構造内のintの2つの連続した配列は、予想どおりにレイアウトされます。 (あなたはmightaの境界を4ではなく3または5に設定し、コンパイラに両方のaを揃えることにより、非常に不自然な反例を設計することができます。そして、16バイト境界のb。標準の厳密な表現を超えて何も仮定しないプログラムを取得しようとする複雑な方法ではなく、static assert(&both_arrays[4] == &s.b[0], "");。これらは実行時のオーバーヘッドを追加せず、アサーション自体でUBをトリガーしない限り、コンパイラがプログラムを壊すようなことをしている場合は失敗します。

両方のサブ配列が連続したメモリ範囲にパックされることを保証するポータブルな方法が必要な場合、またはメモリブロックを別の方法で分割する場合は、memcpy()を使用してコピーできます。

1
Davislor

標準では、プログラムが1つの構造体フィールドで範囲外の配列添え字を使用して別の構造体フィールドのメンバーにアクセスしようとする場合に、実装が行う必要のある制限はありません。したがって、範囲外アクセスは「違法」です厳密に準拠するプログラムではであり、このようなアクセスを利用するプログラムは、同時に100%移植可能でエラーが発生することはありません。一方、多くの実装はそのようなコードの動作を定義しており、そのような実装のみを対象とするプログラムはそのような動作を悪用する可能性があります。

このようなコードには3つの問題があります。

  1. 多くの実装は予測可能な方法で構造をレイアウトしますが、標準では実装は最初の構造メンバー以外の構造メンバーの前に任意のパディングを追加できます。コードはsizeofまたはoffsetofを使用して、構造体のメンバーが期待どおりに配置されるようにしますが、他の2つの問題は残ります。

  2. 次のようなものを考えます:

    if (structPtr->array1[x])
     structPtr->array2[y]++;
    return structPtr->array1[x];
    

    通常、コンパイラは、structPtr->array1[x]を使用すると、2つの配列間のエイリアスに依存するコードの動作が変更される場合でも、「if」条件での前の使用と同じ値が得られると想定します。

  3. array1[]にたとえば4つの要素、コンパイラは次のようなものを与えられます:

    if (x < 4) foo(x);
    structPtr->array1[x]=1;
    

xが4以上の場合は定義されていないので、無条件にfoo(x)を呼び出すことができると結論付けるかもしれません。

残念ながら、プログラムはsizeofまたはoffsetofを使用して構造体レイアウトに驚きがないことを保証できますが、コンパイラーが型#2の最適化を控えると約束するかどうかをテストする方法はありませんまたは#3。さらに、標準は次のような場合に何を意味するかについて少し曖昧です。

struct foo {char array1[4],array2[4]; };

int test(struct foo *p, int i, int x, int y, int z)
{
  if (p->array2[x])
  {
    ((char*)p)[x]++;
    ((char*)(p->array1))[y]++;
    p->array1[z]++;
  }
  return p->array2[x];
}

規格は、zが0..3の範囲にある場合にのみ動作が定義されることをかなり明確にしていますが、その式のp-> arrayの型はchar *(減衰のため)であるため、アクセスのキャストはクリアされませんyを使用すると効果があります。一方、構造体の最初の要素へのポインターをchar*に変換すると、構造体ポインターをchar*に変換するのと同じ結果が得られ、変換された構造体ポインターはその中のすべてのバイトにアクセスできるはずなので、 xは(少なくとも)x = 0..7に対して定義する必要があります[array2のオフセットが4より大きい場合、array2のメンバーをヒットするために必要なxの値に影響しますが、 xの値は、定義された動作でそうすることができます]。

私見、良い解決策は、ポインターの減衰を伴わない方法で配列型に添え字演算子を定義することです。その場合、式p->array[x]および&(p->array1[x])は、xが0..3であると仮定するようにコンパイラーを招待できますが、p->array+xおよび*(p->array+x)は、他の値の可能性。コンパイラーがそれを行うかどうかはわかりませんが、標準では必要ありません。

0
supercat