web-dev-qa-db-ja.com

CおよびC ++が構造体内の配列のメンバーごとの代入をサポートしているのに、一般的にはサポートしていないのはなぜですか?

配列のメンバーごとの割り当てがサポートされていないため、以下が機能しないことを理解しています。

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

私はこれを事実として受け入れ、言語の目的はオープンエンドのフレームワークを提供することであり、配列のコピーなどの実装方法をユーザーに決定させることであると考えました。

ただし、以下は機能します。

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

配列num[3]は、メンバーごとにstruct1のインスタンスから、struct2のインスタンスに割り当てられます。

配列のメンバーごとの割り当てが構造体でサポートされているのに、一般的にはサポートされていないのはなぜですか?

editRoger Pateのスレッド内のコメント 構造体のstd :: string-コピー/割り当ての問題? 答えの一般的な方向を指しているようですが、自分で確認するのに十分な知識がありません。

編集2:多くの優れた回答。私はLuther Blissettを選択します。これは、行動の背後にある哲学的または歴史的根拠についてほとんど疑問に思っていたためですが、James McNellisの関連する仕様書への参照も役に立ちました。

84
ozmo

これが私の見解です:

C言語の開発は、Cでの配列型の進化に関する洞察を提供します。

配列の概要を説明します。

Cの前身であるBとBCPLには、次のような明確な配列型がありませんでした。

_auto V[10] (B)
or 
let V = vec 10 (BCPL)
_

vは、メモリの10「ワード」の未使用領域を指すように初期化される(型指定されていない)ポインタであると宣言します。 Bはすでにポインタの逆参照に_*_を使用し、_[]_の省略表記を使用していました。現在のC/C++と同様に、*(V+i)は_V[i]_を意味します。ただし、Vは配列ではなく、何らかのメモリを指す必要があるポインタです。これは、DennisRitchieが構造体タイプでBを拡張しようとしたときに問題を引き起こしました。彼は、今日のCのように、配列を構造体の一部にしたいと考えていました。

_struct {
    int inumber;
    char name[14];
};
_

しかし、ポインタとしての配列のB、BCPLの概念では、これにはnameフィールドにポインタが含まれている必要があり、これは実行時に初期化内の14バイトのメモリ領域に必要でした。構造体。初期化/レイアウトの問題は、配列に特別な処理を加えることで最終的に解決されました。コンパイラは、配列を含む式を除いて、データへのポインタを実際にマテリアライズすることなく、構造体やスタックなどの配列の場所を追跡します。この処理により、ほぼすべてのBコードを引き続き実行でき、"配列を見るとポインターに変換される"ルールのソースになります。これは互換性ハックであり、オープンサイズなどの配列を許可するため、非常に便利であることがわかりました。

そして、配列を割り当てることができない理由は次のとおりです。配列はBのポインターであるため、次のように書くことができます。

_auto V[10];
V=V+5;
_

「アレイ」をリベースします。配列変数のベースが左辺値ではなくなったため、これは無意味になりました。したがって、この割り当ては許可されませんでした。これは、このリベースを実行したいくつかのプログラムをキャッチするのに役立ちました宣言された配列で。そして、この概念は固執しました。配列は、C型システムを一流に引用するように設計されたことがないため、ほとんどの場合、使用するとポインターになる特別な獣として扱われました。また、特定の観点から(C配列が失敗したハックであることを無視します)、配列の割り当てを禁止することには意味があります。開いている配列または配列関数パラメーターは、サイズ情報のないポインターとして扱われます。コンパイラーには、それらの配列割り当てを生成するための情報がなく、互換性の理由からポインター割り当てが必要でした。宣言された配列に配列代入を導入すると、実際に問題を解決せずに、偽の代入(a = baポインタ代入または要素ごとのコピーですか?)やその他の問題(配列を値で渡す方法)を通じてバグが発生します-すべてを作成するだけですmemcpyで明示的に!

_/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}
_

1978年のCの改訂で構造体の割り当てが追加されたとき、これは変わりませんでした( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf )。レコードwere Cでは異なるタイプですが、初期のK&R Cではそれらを割り当てることができませんでした。memcpyを使用してメンバーごとにコピーする必要があり、関数パラメーターとしてそれらへのポインターのみを渡すことができました。アシグメント(およびパラメーターの受け渡し)は、構造体の生のメモリーのmemcpyとして単純に定義され、既存のコードを壊すことができなかったため、すぐに採用されました。意図しない副作用として、これは暗黙的にある種の配列割り当てを導入しましたが、これは構造内のどこかで発生したため、配列の使用方法に実際に問題を引き起こすことはありませんでした。

45

代入演算子に関して、C++標準は次のように述べています(C++03§5.17/ 1):

いくつかの代入演算子があります...すべて左オペランドとして変更可能な左辺値が必要です

配列は変更可能な左辺値ではありません。

ただし、クラスタイプオブジェクトへの割り当ては特別に定義されています(§5.17/ 4)。

クラスのオブジェクトへの割り当ては、コピー割り当て演算子によって定義されます。

したがって、クラスに対して暗黙的に宣言されたコピー割り当て演算子が何をするかを確認します(§12.8/ 13)。

クラスXの暗黙的に定義されたコピー代入演算子は、そのサブオブジェクトのメンバーごとの代入を実行します。 ...各サブオブジェクトは、そのタイプに適した方法で割り当てられます。
.。
-サブオブジェクトが配列の場合、各要素は要素タイプに適した方法で割り当てられます
.。

したがって、クラス型オブジェクトの場合、配列は正しくコピーされます。ユーザーが宣言したコピー割り当て演算子を指定した場合、これを利用することはできず、配列を要素ごとにコピーする必要があることに注意してください。


推論はCでも同様です(C99§6.5.16/ 2):

代入演算子は、その左オペランドとして変更可能な左辺値を持つものとします。

そして§6.3.2.1/ 1:

変更可能な左辺値は、配列型を持たない左辺値です... [他の制約が続きます]

Cでは、割り当てはC++(§6.5.16.1/ 2)よりもはるかに簡単です。

単純代入(=)では、右オペランドの値が代入式の型に変換され、左オペランドで指定されたオブジェクトに格納されている値に置き換わります。

構造体タイプのオブジェクトを割り当てるには、左と右のオペランドが同じタイプである必要があるため、右のオペランドの値は単純に左のオペランドにコピーされます。

28
James McNellis

このリンク: http://www2.research.att.com/~bs/bs_faq2.html 配列の割り当てに関するセクションがあります。

配列に関する2つの基本的な問題は、

  • 配列はそれ自体のサイズを知りません
  • 配列の名前は、わずかな挑発で最初の要素へのポインタに変換されます

これが配列と構造体の根本的な違いだと思います。配列変数は、自己知識が限られた低レベルのデータ要素です。基本的に、そのメモリのチャンクとそれにインデックスを付ける方法。

したがって、コンパイラはint a [10]とintb [20]の違いを区別できません。

ただし、構造体には同じあいまいさはありません。

2
Scott Turley

Cで配列を強化するためにこれ以上の努力がなされなかったもう一つの理由は、おそらく配列の割り当てが役に立たないということですthat。 Cでは構造体でラップすることで簡単に実現できますが(構造体のアドレスを配列のアドレスまたは配列の最初の要素のアドレスにキャストしてさらに処理することもできます)、この機能はほとんど使用されません。理由の1つは、異なるサイズの配列には互換性がないため、割り当ての利点、または関連して、値による関数への受け渡しが制限されることです。

配列がファーストクラスタイプである言語の配列パラメーターを持つほとんどの関数は、任意のサイズの配列用に記述されています。次に、関数は通常、配列が提供する情報である、指定された数の要素を反復処理します。 (Cでは、もちろん、イディオムはポインターと個別の要素数を渡すことです。)1つの特定のサイズの配列のみを受け入れる関数は、それほど頻繁には必要ないため、多くのことを見逃すことはありません。 (これは、C++テンプレートの場合と同様に、発生する配列サイズに対して個別の関数を生成するためにコンパイラーに任せることができる場合に変更されます。これがstd::arrayが役立つ理由です。)

私は知っています、答えた人は皆C/C++の専門家です。しかし、これが主な理由だと思いました。

num2 = num1;

ここでは、配列のベースアドレスを変更しようとしていますが、これは許可されていません。

そしてもちろん、struct2 = struct1;

ここで、オブジェクトstruct1は別のオブジェクトに割り当てられています。

0
nsivakr