Pourquoi C et C ++ prennent-ils en charge l'affectation par membre de tableaux dans les structures, mais pas en général?

87

Je comprends que l'attribution de tableaux par membre n'est pas prise en charge, de sorte que ce qui suit ne fonctionnera pas:

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

Je viens d'accepter cela comme un fait, en pensant que le but du langage est de fournir un cadre ouvert et de laisser l'utilisateur décider comment implémenter quelque chose comme la copie d'un tableau.

Cependant, ce qui suit fonctionne:

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

Le tableau num[3]est affecté par membre à partir de son instance dans struct1, dans son instance dans struct2.

Pourquoi l'affectation des tableaux par membre est-elle prise en charge pour les structures, mais pas en général?

edit : Commentaire de Roger Pate dans le thread std :: string dans la structure - Problèmes de copie / affectation? semble indiquer la direction générale de la réponse, mais je n'en sais pas assez pour la confirmer moi-même.

edit 2 : Beaucoup d'excellentes réponses. J'ai choisi Luther Blissett parce que je m'interrogeais surtout sur la logique philosophique ou historique derrière le comportement, mais la référence de James McNellis à la documentation de spécification associée était également utile.

ozmo
la source
6
Je fais que cela ait à la fois C et C ++ comme balises, car cela provient de C. Aussi, bonne question.
GManNickG
4
Il peut être intéressant de noter qu'il y a longtemps en C, l'attribution de structure n'était généralement pas possible et vous deviez utiliser memcpy()ou similaire.
ggg
Juste un peu FYI ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) et maintenant std::array( en.cppreference.com/w/cpp/container/array ) sont des alternatives compatibles STL au vieux tableaux C en désordre. Ils prennent en charge l'affectation de copie.
Emile Cormier
@EmileCormier Et ils sont - tada! - les structures autour des tableaux.
Peter - Réintègre Monica

Réponses:

46

Voici mon point de vue:

Le développement du langage C offre un aperçu de l'évolution du type de tableau en C:

Je vais essayer de décrire le tableau:

Les précurseurs B et BCPL de C n'avaient pas de type de tableau distinct, une déclaration comme:

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

déclarerait V comme étant un pointeur (non typé) qui est initialisé pour pointer vers une région inutilisée de 10 "mots" de mémoire. B déjà utilisé *pour pointer déréférencement et a la [] notation de sténographie, *(V+i)signifiait V[i], comme dans aujourd'hui C / C. Cependant, ce Vn'est pas un tableau, c'est toujours un pointeur qui doit pointer vers une certaine mémoire. Cela a causé des problèmes lorsque Dennis Ritchie a essayé d'étendre B avec des types de structure. Il voulait que les tableaux fassent partie des structures, comme en C aujourd'hui:

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

Mais avec le concept B, BCPL des tableaux en tant que pointeurs, cela aurait exigé que le namechamp contienne un pointeur qui devait être initialisé à l'exécution sur une région mémoire de 14 octets dans la structure. Le problème d'initialisation / mise en page a finalement été résolu en donnant aux tableaux un traitement spécial: le compilateur suivrait l'emplacement des tableaux dans les structures, sur la pile, etc. sans nécessiter réellement la matérialisation du pointeur vers les données, sauf dans les expressions qui impliquent les tableaux. Ce traitement permettait à presque tout le code B de continuer à s'exécuter et est la source de la règle «les tableaux convertis en pointeur si vous les regardez» . C'est un hack de compatibilité, qui s'est avéré très pratique, car il permettait des tableaux de taille ouverte, etc.

Et voici ma supposition pourquoi le tableau ne peut pas être attribué: Puisque les tableaux étaient des pointeurs en B, vous pouvez simplement écrire:

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

pour rebaser un "tableau". Cela n'avait plus de sens, car la base d'une variable de tableau n'était plus une lvalue. Donc, cette attribution a été interdite, ce qui a aidé à attraper les quelques programmes qui ont fait ce rebasage sur des tableaux déclarés. Et puis cette notion est restée: comme les tableaux n'ont jamais été conçus pour être des citations de première classe du système de type C, ils étaient principalement traités comme des bêtes spéciales qui deviennent des pointeurs si vous les utilisez. Et d'un certain point de vue (qui ignore que les C-arrays sont un hack bâclé), interdire l'affectation de tableaux a encore du sens: un tableau ouvert ou un paramètre de fonction de tableau est traité comme un pointeur sans informations de taille. Le compilateur ne dispose pas des informations pour générer une affectation de tableau pour eux et l'affectation de pointeur était requise pour des raisons de compatibilité.

/* 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
}

Cela n'a pas changé lorsqu'une révision de C en 1978 a ajouté l'affectation de structure ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Même si les enregistrements étaient des types distincts en C, il n'était pas possible de les affecter au début de K&R C. Vous deviez les copier membre par membre avec memcpy et vous ne pouviez leur passer que des pointeurs comme paramètres de fonction. L'assignation (et le passage des paramètres) était maintenant simplement définie comme le memcpy de la mémoire brute de la structure et comme cela ne pouvait pas casser le code existant, il était facilement adopté. En tant qu'effet secondaire involontaire, cela introduisit implicitement une sorte d'assignation de tableau, mais cela se produisait quelque part dans une structure, donc cela ne pouvait pas vraiment poser de problèmes avec la façon dont les tableaux étaient utilisés.

Mainframe nordique
la source
C'est dommage que C n'ait pas défini de syntaxe, par exemple int[10] c;pour que la lvalue cse comporte comme un tableau de dix éléments, plutôt que comme un pointeur vers le premier élément d'un tableau de dix éléments. Il y a quelques situations où il est utile de pouvoir créer un typedef qui alloue de l'espace lorsqu'il est utilisé pour une variable, mais passe un pointeur lorsqu'il est utilisé comme argument de fonction, mais l'incapacité d'avoir une valeur de type tableau est une faiblesse sémantique importante dans la langue.
supercat
Au lieu de dire "pointeur qui doit pointer vers une mémoire", le point important est que le pointeur lui-même doit être stocké en mémoire comme un pointeur normal. Cela apparaît dans votre explication ultérieure, mais je pense que cela met mieux en évidence la principale différence. (En C moderne, le nom d'une variable de tableau fait référence à un bloc de mémoire, ce n'est donc pas la différence. C'est que le pointeur lui-même n'est logiquement stocké nulle part dans la machine abstraite.)
Peter Cordes
Voir l'aversion de C pour les tableaux pour un bon résumé de l'histoire.
Peter Cordes
31

Concernant les opérateurs d'affectation, le standard C ++ dit ce qui suit (C ++ 03 §5.17 / 1):

Il existe plusieurs opérateurs d'affectation ... tous nécessitent une lvalue modifiable comme opérande gauche

Un tableau n'est pas une lvalue modifiable.

Cependant, l'affectation à un objet de type classe est définie spécialement (§5.17 / 4):

L'affectation aux objets d'une classe est définie par l'opérateur d'affectation de copie.

Donc, nous regardons pour voir ce que fait l'opérateur d'affectation de copie implicitement déclaré pour une classe (§12.8 / 13):

L'opérateur d'affectation de copie défini implicitement pour la classe X effectue une affectation par membre de ses sous-objets. ... Chaque sous-objet est affecté de la manière appropriée à son type:
...
- si le sous-objet est un tableau, chaque élément est affecté, de la manière appropriée au type d'élément
...

Ainsi, pour un objet de type classe, les tableaux sont copiés correctement. Notez que si vous fournissez un opérateur d'affectation de copie déclaré par l'utilisateur, vous ne pouvez pas en profiter et vous devrez copier le tableau élément par élément.


Le raisonnement est similaire en C (C99 §6.5.16 / 2):

Un opérateur d'assignation doit avoir une valeur l modifiable comme opérande de gauche.

Et §6.3.2.1 / 1:

Une lvalue modifiable est une lvalue qui n'a pas de type tableau ... [d'autres contraintes suivent]

En C, l'affectation est beaucoup plus simple qu'en C ++ (§6.5.16.1 / 2):

Dans l'affectation simple (=), la valeur de l'opérande de droite est convertie en type de l'expression d'affectation et remplace la valeur stockée dans l'objet désigné par l'opérande de gauche.

Pour l'affectation d'objets de type struct, les opérandes gauche et droit doivent avoir le même type, de sorte que la valeur de l'opérande droit est simplement copiée dans l'opérande gauche.

James McNellis
la source
1
Pourquoi les tableaux sont-ils immuables? Ou plutôt, pourquoi l'affectation n'est-elle pas définie spécialement pour les tableaux comme c'est le cas quand elle est dans un type de classe?
GManNickG
1
@GMan: C'est la question la plus intéressante, n'est-ce pas. Pour C ++, la réponse est probablement "parce que c'est comme ça en C", et pour C, je suppose que c'est juste à cause de la façon dont le langage a évolué (c'est-à-dire, la raison est historique, pas technique), mais je n'étais pas en vie quand la plupart de cela a eu lieu, alors je laisserai à quelqu'un de plus compétent le soin de répondre à cette partie :-P (FWIW, je ne trouve rien dans les documents de justification C90 ou C99).
James McNellis
2
Est-ce que quelqu'un sait où se trouve la définition de «lvalue modifiable» dans le standard C ++ 03? Cela devrait être au §3.10. L'index dit qu'il est défini sur cette page, mais ce n'est pas le cas. La note (non normative) au §8.3.4 / 5 dit "Les objets de types tableau ne peuvent pas être modifiés, voir 3.10," mais le §3.10 n'utilise pas une seule fois le mot "tableau".
James McNellis
@James: Je faisais la même chose. Il semble faire référence à une définition supprimée. Et oui, j'ai toujours voulu connaître la vraie raison derrière tout cela, mais cela semble un mystère. J'ai entendu des choses comme "empêcher les gens d'être inefficaces en attribuant accidentellement des tableaux", mais c'est ridicule.
GManNickG
1
@GMan, James: Il y a eu récemment une discussion sur comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… si vous l'avez manqué et êtes toujours intéressé. Apparemment, ce n'est pas parce qu'un tableau n'est pas une lvalue modifiable (un tableau est certainement une lvalue et toutes les lvalues ​​non const sont modifiables), mais parce que =nécessite une rvalue sur le RHS et un tableau ne peut pas être une rvalue ! La conversion lvalue-to-rvalue est interdite pour les tableaux, remplacée par lvalue-to-pointer. static_castn'est pas meilleur pour créer une rvalue car elle est définie dans les mêmes termes.
Potatoswatter
2

Dans ce lien: http://www2.research.att.com/~bs/bs_faq2.html, il y a une section sur l'affectation des tableaux:

Les deux problèmes fondamentaux avec les tableaux sont que

  • un tableau ne connaît pas sa propre taille
  • le nom d'un tableau se convertit en pointeur sur son premier élément à la moindre provocation

Et je pense que c'est la différence fondamentale entre les tableaux et les structures. Une variable de tableau est un élément de données de bas niveau avec une connaissance de soi limitée. Fondamentalement, c'est un morceau de mémoire et un moyen de s'y indexer.

Ainsi, le compilateur ne peut pas faire la différence entre int a [10] et int b [20].

Les structures, cependant, n'ont pas la même ambiguïté.

Scott Turley
la source
3
Cette page parle de passer des tableaux aux fonctions (ce qui ne peut pas être fait, donc c'est juste un pointeur, ce qu'il veut dire quand il dit qu'il perd sa taille). Cela n'a rien à voir avec l'affectation de tableaux à des tableaux. Et non, une variable tableau n'est pas simplement "vraiment" un pointeur vers le premier élément, c'est un tableau. Les tableaux ne sont pas des pointeurs.
GManNickG
Merci pour le commentaire, mais quand j'ai lu cette section de l'article, il dit d'emblée que les tableaux ne connaissent pas sa propre taille, puis utilise un exemple où les tableaux sont passés comme arguments pour illustrer ce fait. Ainsi, lorsque les tableaux sont passés comme arguments, ont-ils perdu les informations sur leur taille ou n'ont-ils jamais eu les informations pour commencer. J'ai assumé ce dernier.
Scott Turley
3
Le compilateur peut faire la différence entre deux tableaux de taille différente - essayez d' imprimer par sizeof(a)rapport sizeof(b)ou passer aà void f(int (&)[20]);.
Georg Fritzsche
Il est important de comprendre que chaque taille de tableau constitue son propre type. Les règles de passage des paramètres garantissent que vous pouvez écrire des fonctions "génériques" de pauvres qui acceptent des arguments de tableau de n'importe quelle taille, au prix de devoir passer la taille séparément. Si ce n'était pas le cas (et en C ++, vous pouvez - et devez! - définir des paramètres de référence pour des tableaux de taille spécifique), vous auriez besoin d'une fonction spécifique pour chaque taille différente, ce qui n'a aucun sens. J'ai écrit à ce sujet dans un autre post .
Peter - Réintègre Monica
0

Je sais, tous ceux qui ont répondu sont des experts en C / C ++. Mais j'ai pensé que c'était la raison principale.

num2 = num1;

Ici, vous essayez de changer l'adresse de base du tableau, ce qui n'est pas autorisé.

et bien sûr, struct2 = struct1;

Ici, l'objet struct1 est affecté à un autre objet.

nsivakr
la source
Et l'affectation de structures finira par affecter le membre du tableau, ce qui soulève exactement la même question. Pourquoi l'un est autorisé et pas l'autre, alors qu'il s'agit d'un tableau dans les deux situations?
GManNickG
1
D'accord. Mais le premier est empêché par le compilateur (num2 = num1). Le second n'est pas empêché par le compilateur. Cela fait une énorme différence.
nsivakr
Si les tableaux étaient assignables, num2 = num1ils se comporteraient parfaitement. Les éléments de num2auraient la même valeur que l'élément correspondant de num1.
juanchopanza
0

Une autre raison pour laquelle aucun effort supplémentaire n'a été fait pour renforcer les tableaux en C est probablement que l'affectation de tableaux ne serait pas si utile. Même si cela peut être facilement réalisé en C en l'enveloppant dans une structure (et l'adresse de la structure peut être simplement convertie à l'adresse du tableau ou même à l'adresse du premier élément du tableau pour un traitement ultérieur), cette fonctionnalité est rarement utilisée. L'une des raisons est que les tableaux de tailles différentes sont incompatibles, ce qui limite les avantages de l'affectation ou, en relation, du passage aux fonctions par valeur.

La plupart des fonctions avec des paramètres de tableau dans les langages où les tableaux sont des types de première classe sont écrites pour des tableaux de taille arbitraire. La fonction effectue ensuite une itération sur le nombre d'éléments donné, une information fournie par le tableau. (En C, l'idiome est, bien sûr, de passer un pointeur et un nombre d'éléments séparés.) Une fonction qui accepte un tableau d'une seule taille spécifique n'est pas nécessaire aussi souvent, donc il ne manque pas grand chose. (Cela change lorsque vous pouvez laisser au compilateur le soin de générer une fonction distincte pour toute taille de tableau, comme avec les modèles C ++; c'est la raison pour laquelle std::arrayest utile.)

Peter - Réintégrer Monica
la source