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


87

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

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

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

編集ロジャーパテのスレッドにコメントのstd ::構造体の文字列-コピー/割り当ての問題?答えの一般的な方向を指しているようですが、私はそれを自分で確認するのに十分なことを知りません。

編集2:多くの優れた応答。行動の背後にある哲学的または歴史的根拠についてほとんど疑問に思っていたので、ルーサー・ブリセットを選びましたが、関連する仕様書へのジェームズ・マクネリスの参照も役に立ちました。


6
これはCに由来するため、タグとしてCとC ++の両方を使用するようにしています。また、良い質問です。
GManNickG 2010

4
昔のCでは、構造体の割り当ては一般的に不可能であり、使用するmemcpy()必要があったことは注目に値するかもしれません。
ggg 2010

参考までに... boost::arrayboost.org/doc/libs/release/doc/html/array.html)と現在std::arrayen.cppreference.com/w/cpp/container/array)は、STL互換の代替手段です。乱雑な古いC配列。それらはコピー割り当てをサポートします。
Emile Cormier

@EmileCormierそして彼らは-多田です!-配列の周りの構造。
ピーター-モニカを復活させる

回答:


46

これが私の見解です:

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

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

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

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

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

struct {
    int inumber;
    char name[14];
};

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

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

auto V[10];
V=V+5;

「アレイ」をリベースします。配列変数のベースが左辺値ではなくなったため、これは無意味になりました。したがって、この割り当ては許可されませんでした。これは、宣言された配列に基づいてこれを実行したいくつかのプログラムをキャッチするのに役立ちました。。そして、この概念は固執しました。配列は、C型システムを一流に引用するように設計されたことがないため、ほとんどの場合、使用するとポインターになる特別な獣として扱われました。また、特定の観点から(C配列が失敗したハックであることを無視します)、配列の割り当てを禁止することには意味があります。開いている配列または配列関数パラメーターは、サイズ情報のないポインターとして扱われます。コンパイラーには、それらの配列割り当てを生成するための情報がなく、互換性の理由からポインター割り当てが必要でした。

/* 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)。レコードCで異なるタイプでしたが、初期のK&R Cではそれらを割り当てることができませんでした。memcpyを使用してメンバーごとにレコードをコピーする必要があり、関数パラメーターとしてレコードへのポインターのみを渡すことができました。アシグメント(およびパラメーターの受け渡し)は、構造体の生のメモリーのmemcpyとして単純に定義され、既存のコードを壊すことができなかったため、すぐに採用されました。意図しない副作用として、これは暗黙的にある種の配列割り当てを導入しましたが、これは構造内のどこかで発生したため、配列の使用方法に実際に問題を引き起こすことはありませんでした。


Cが構文を定義しなかったのは残念です。たとえばint[10] c;、左辺値cを10項目の配列の最初の項目へのポインターとしてではなく、10項目の配列として動作させるようにしました。変数に使用するとスペースを割り当て、関数の引数として使用するとポインターを渡すtypedefを作成できると便利な状況がいくつかありますが、配列型の値を持てないことが意味上の重大な弱点です。言語で。
スーパーキャット2015

「あるメモリを指す必要のあるポインタ」と言う代わりに、重要な点は、ポインタ自体を通常のポインタのようにメモリに格納する必要があるということです。これは後の説明で出くわしますが、それは重要な違いをよりよく強調していると思います。(最近のCでは、配列変数の名前はメモリのブロックを参照しているため、違いはありません。ポインタ自体が抽象マシンのどこにも論理的に格納されていないということです。)
Peter Cordes 2018

歴史の素晴らしい要約については、Cの配列に対する嫌悪感を参照してください。
PeterCordes18年

31

代入演算子に関して、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)よりもはるかに簡単です。

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

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


1
配列が不変なのはなぜですか?むしろ、クラス型の場合のように、配列に対して特別に割り当てが定義されていないのはなぜですか?
GManNickG 2010

1
@GMan:それはもっと興味深い質問ですね。C ++の場合、答えはおそらく「それがCであるため」であり、Cの場合、それは言語がどのように進化したか(つまり、理由は技術的ではなく歴史的である)によるものだと思いますが、私は生きていませんでしたそのほとんどが起こったとき、私はその部分に答えるのにもっと知識のある人に任せます:-P(FWIW、C90またはC99の理論的根拠文書には何も見つかりません)。
James McNellis 2010

2
「変更可能な左辺値」の定義がC ++ 03標準のどこにあるか知っている人はいますか?それ§3.10にあるべきです。インデックスはそれがそのページで定義されていると言っていますが、そうではありません。§8.3.4/ 5の(非規範的な)注記には、「配列型のオブジェクトは変更できません。3.10を参照してください」と記載されていますが、§3.10では「配列」という単語は一度も使用されていません。
James McNellis 2010

@ジェームズ:私はちょうど同じことをしていました。削除された定義を参照しているようです。そして、ええ、私はいつもその背後にある本当の理由を知りたいと思っていましたが、それは謎のようです。「誤って配列を割り当てて人が非効率になるのを防ぐ」などと聞いたことがありますが、それはばかげています。
GManNickG 2010

1
@ GMan、James:最近、comp.lang.c ++ groups.google.com/group/ comp.lang.c++ /browse_frm/thread/…を見逃していて、まだ興味がある場合は、ディスカッションがありました。どうやらそれは、配列が変更可能な左辺値ではないので(配列は確かに左辺値であり、すべての非const左辺値が変更されている)ではないが、ため=必要と右辺値RHSを、配列をすることはできません右辺値!左辺値から右辺値への変換は配列では禁止されており、左辺値からポインターに置き換えられます。static_cast同じ用語で定義されているため、右辺値の作成は得意ではありません。
ポテトスワッター2010

2

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

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

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

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

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

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


3
そのページでは、配列を関数に渡すことについて説明しています(これは実行できないため、単なるポインターであり、サイズが失われたと彼が言ったときの意味です)。これは、配列を配列に割り当てることとは何の関係もありません。いいえ、配列変数は最初の要素への「実際の」ポインタではなく、配列です。配列はポインタではありません。
GManNickG 2010

コメントをありがとう、しかし私が記事のそのセクションを読んだとき、彼は配列がそれ自身のサイズを知らないと前もって言って、そしてその事実を説明するために配列が引数として渡される例を使用します。したがって、配列が引数として渡されると、サイズに関する情報が失われるのでしょうか、それとも最初から情報がないのでしょうか。私は後者を想定しました。
スコットターリー2010

3
コンパイラは、二つの異なるサイズのアレイ間の違いを見分けることができます-印刷してみてくださいsizeof(a)対をsizeof(b)か渡すことavoid f(int (&)[20]);
Georg Fritzsche 2010年

各配列サイズが独自の型を構成することを理解することが重要です。パラメータの受け渡しのルールにより、サイズを個別に渡す必要がありますが、任意のサイズの配列引数を受け取る貧乏人の「ジェネリック」関数を記述できます。そうでない場合(そしてC ++では特定のサイズの配列への参照パラメーターを定義できます-そしてそうしなければなりません!)、異なるサイズごとに特定の関数が必要になりますが、明らかに意味がありません。私はそれについて別の投稿に書いた。
ピーター-モニカを復活させる

0

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

num2 = num1;

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

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

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


そして、構造体を割り当てると、最終的に配列メンバーが割り当てられ、まったく同じ質問が発生します。両方の状況で配列であるのに、一方が許可され、もう一方が許可されないのはなぜですか?
GManNickG 2010

1
同意しました。しかし、最初のものはコンパイラーによって防止されます(num2 = num1)。2つ目は、コンパイラーによって防止されません。それは大きな違いを生みます。
nsivakr 2010

配列が割り当て可能であれば、num2 = num1完全に適切に動作します。の要素はnum2、の対応する要素と同じ値になりnum1ます。
juanchopanza 2014

0

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.