Por que C e C ++ oferecem suporte à atribuição de membros a membros de matrizes em estruturas, mas não de maneira geral?

87

Eu entendo que a atribuição de matrizes a membros não é compatível, de modo que o seguinte não funcionará:

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

Eu apenas aceitei isso como um fato, imaginando que o objetivo da linguagem é fornecer uma estrutura aberta e deixar o usuário decidir como implementar algo como a cópia de um array.

No entanto, o seguinte funciona:

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

O array num[3]é atribuído por membro a partir de sua instância em struct1, para sua instância em struct2.

Por que a atribuição de matrizes por membro é compatível com structs, mas não em geral?

editar : Comentário de Roger Pate no tópico std :: string em struct - Problemas de cópia / atribuição? parece apontar na direção geral da resposta, mas não sei o suficiente para confirmá-la sozinho.

editar 2 : Muitas respostas excelentes. Eu escolhi Luther Blissett porque estava pensando principalmente sobre a lógica filosófica ou histórica por trás do comportamento, mas a referência de James McNellis à documentação de especificação relacionada também foi útil.

Ozmo
fonte
6
Estou fazendo isso ter C e C ++ como tags, porque se origina de C. Além disso, boa pergunta.
GManNickG
4
Pode ser interessante notar que há muito tempo em C, a atribuição de estrutura geralmente não era possível e você tinha que usar memcpy()ou algo semelhante.
ggg
Apenas um pouco FYI ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) e agora std::array( en.cppreference.com/w/cpp/container/array ) são alternativas compatíveis com STL para o velhos arrays C bagunçados. Eles apóiam a atribuição de cópias.
Emile Cormier
@EmileCormier E eles são - tada! - estruturas em torno de matrizes.
Peter - Reintegrar Monica de

Respostas:

46

Aqui está minha opinião sobre isso:

O desenvolvimento da linguagem C oferece alguns insights sobre a evolução do tipo de array em C:

Vou tentar delinear a coisa do array:

Os precursores B e BCPL de C não tinham um tipo de array distinto, uma declaração como:

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

declararia que V é um ponteiro (não digitado) que é inicializado para apontar para uma região não utilizada de 10 "palavras" de memória. B já utilizado *para dereferencing ponteiro e teve a [] notação mão curta, *(V+i)significava V[i], assim como hoje C / C ++. Porém, Vnão é um array, é ainda um ponteiro que deve apontar para alguma memória. Isso causou problemas quando Dennis Ritchie tentou estender B com tipos de estrutura. Ele queria que os arrays fizessem parte dos structs, como em C hoje:

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

Mas com o conceito B, BCPL de matrizes como ponteiros, isso exigiria que o namecampo contivesse um ponteiro que deveria ser inicializado em tempo de execução para uma região de memória de 14 bytes dentro da estrutura. O problema de inicialização / layout foi eventualmente resolvido dando aos arrays um tratamento especial: O compilador rastreia a localização dos arrays nas estruturas, na pilha, etc. sem realmente exigir que o ponteiro para os dados se materializem, exceto em expressões que envolvem os arrays. Esse tratamento permitiu que quase todo o código B ainda rodasse e é a fonte da regra "arrays convertidos em ponteiro se você olhar para eles" . É um hack de compatibilidade, que acabou sendo muito útil, pois permitia arrays de tamanho aberto etc.

E aqui está o meu palpite de por que array não pode ser atribuído: como arrays eram ponteiros em B, você poderia simplesmente escrever:

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

para rebase uma "matriz". Isso agora não tinha sentido, porque a base de uma variável de matriz não era mais um lvalue. Portanto, essa atribuição foi rejeitada, o que ajudou a capturar os poucos programas que faziam essa rebase nas matrizes declaradas. E então essa noção pegou: como os arrays nunca foram projetados para serem citados de primeira classe no sistema de tipo C, eles foram tratados como bestas especiais que se tornam ponteiros se você usá-los. E de um certo ponto de vista (que ignora que C-arrays são um hack malfeito), desabilitar a atribuição de array ainda faz algum sentido: um array aberto ou um parâmetro de função de array é tratado como um ponteiro sem informações de tamanho. O compilador não tem as informações para gerar uma atribuição de array para eles e a atribuição do ponteiro foi necessária por motivos de compatibilidade.

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

Isso não mudou quando uma revisão de C em 1978 adicionou atribuição de estrutura ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Mesmo que os registros fossem tipos distintos em C, não era possível atribuí-los no K&R C. Você tinha que copiá-los por meio de membros com memcpy e podia passar apenas ponteiros para eles como parâmetros de função. A atribuição (e a passagem de parâmetro) agora era simplesmente definida como o memcpy da memória bruta da estrutura e, como isso não poderia quebrar o código existente, foi prontamente adaptado. Como um efeito colateral não intencional, isso implicitamente introduziu algum tipo de atribuição de array, mas isso aconteceu em algum lugar dentro de uma estrutura, então isso não poderia realmente apresentar problemas com a maneira como os arrays eram usados.

Nordic Mainframe
fonte
É uma pena que C não definiu uma sintaxe, por exemplo, int[10] c;para fazer o lvalue cse comportar como um array de dez itens, ao invés de um ponteiro para o primeiro item de um array de dez itens. Existem algumas situações em que é útil ser capaz de criar um typedef que aloca espaço quando usado para uma variável, mas passa um ponteiro quando usado como um argumento de função, mas a incapacidade de ter um valor do tipo array é uma fraqueza semântica significativa no idioma.
supercat de
Em vez de dizer "ponteiro que deve apontar para alguma memória", o ponto importante é que o próprio ponteiro deve ser armazenado na memória como um ponteiro normal. Isso transparece em sua explicação posterior, mas acho que destaca melhor a diferença principal. (No C moderno, o nome de uma variável de array se refere a um bloco de memória, então essa não é a diferença. É que o ponteiro em si não é armazenado logicamente em qualquer lugar na máquina abstrata.)
Peter Cordes
Veja a aversão de C a matrizes para um bom resumo da história.
Peter Cordes
31

Com relação aos operadores de atribuição, o padrão C ++ diz o seguinte (C ++ 03 §5.17 / 1):

Existem vários operadores de atribuição ... todos requerem um lvalue modificável como seu operando esquerdo

Uma matriz não é um lvalue modificável.

No entanto, a atribuição a um objeto de tipo de classe é definida especialmente (§5.17 / 4):

A atribuição a objetos de uma classe é definida pelo operador de atribuição de cópia.

Portanto, procuramos ver o que faz o operador de atribuição de cópia declarado implicitamente para uma classe (§12.8 / 13):

O operador de atribuição de cópia definido implicitamente para a classe X executa a atribuição de membro para seus subobjetos. ... Cada subobjeto é atribuído da maneira apropriada ao seu tipo:
...
- se o subobjeto for uma matriz, cada elemento é atribuído, da maneira apropriada para o tipo de elemento
...

Portanto, para um objeto de tipo de classe, os arrays são copiados corretamente. Observe que, se você fornecer um operador de atribuição de cópia declarado pelo usuário, não poderá tirar vantagem disso e terá que copiar o array elemento por elemento.


O raciocínio é semelhante em C (C99 §6.5.16 / 2):

Um operador de atribuição deve ter um lvalue modi fi cável como seu operando esquerdo.

E §6.3.2.1 / 1:

Um lvalue modi fi cável é um lvalue que não tem um tipo de array ... [seguem-se outras restrições]

Em C, a atribuição é muito mais simples do que em C ++ (§6.5.16.1 / 2):

Na atribuição simples (=), o valor do operando direito é convertido para o tipo da expressão de atribuição e substitui o valor armazenado no objeto designado pelo operando esquerdo.

Para atribuição de objetos do tipo struct, os operandos esquerdo e direito devem ter o mesmo tipo, portanto, o valor do operando direito é simplesmente copiado para o operando esquerdo.

James McNellis
fonte
1
Por que os arrays são imutáveis? Ou melhor, por que a atribuição não é definida especialmente para arrays como quando está em um tipo de classe?
GManNickG
1
@GMan: Essa é a pergunta mais interessante, não é? Para C ++, a resposta é provavelmente "porque é assim que é em C", e para C, acho que é apenas devido à forma como a linguagem evoluiu (ou seja, o motivo é histórico, não técnico), mas eu não estava vivo quando a maior parte disso aconteceu, então vou deixar para alguém mais bem informado responder a essa parte :-P (FWIW, não consigo encontrar nada nos documentos de justificativa C90 ou C99).
James McNellis
2
Alguém sabe onde está a definição de "valor modificável" no padrão C ++ 03? Ele deve estar em §3.10. O índice diz que está definido nessa página, mas não está. A nota (não normativa) em §8.3.4 / 5 diz "Objetos de tipos de array não podem ser modificados, consulte 3.10", mas §3.10 não usa nenhuma vez a palavra "array".
James McNellis
@James: Eu estava fazendo o mesmo. Parece referir-se a uma definição removida. E sim, sempre quis saber a verdadeira razão por trás de tudo, mas parece um mistério. Já ouvi coisas como "impedir que as pessoas sejam ineficientes atribuindo matrizes acidentalmente", mas isso é ridículo.
GManNickG
1
@GMan, James: Recentemente, houve uma discussão em comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… se você perdeu e ainda está interessado. Aparentemente, não é porque um array não é um lvalue modificável (um array certamente é um lvalue e todos os valores l não constantes são modificáveis), mas porque =requer um rvalue no RHS e um array não pode ser um rvalue ! A conversão de lvalue-para-rvalue é proibida para matrizes, substituída por lvalue-para-ponteiro. static_castnão é melhor em fazer um rvalue porque é definido nos mesmos termos.
Potatoswatter
2

Neste link: http://www2.research.att.com/~bs/bs_faq2.html há uma seção sobre atribuição de matriz:

Os dois problemas fundamentais com matrizes são que

  • uma matriz não conhece seu próprio tamanho
  • o nome de um array se converte em um ponteiro para o seu primeiro elemento à menor provocação

E eu acho que essa é a diferença fundamental entre arrays e structs. Uma variável de matriz é um elemento de dados de baixo nível com autoconhecimento limitado. Fundamentalmente, é um pedaço de memória e uma maneira de indexar nele.

Portanto, o compilador não pode dizer a diferença entre int a [10] e int b [20].

As estruturas, no entanto, não têm a mesma ambigüidade.

Scott Turley
fonte
3
Essa página fala sobre como passar arrays para funções (o que não pode ser feito, então é apenas um ponteiro, que é o que ele quer dizer quando diz que perde o tamanho). Isso não tem nada a ver com a atribuição de arrays a arrays. E não, uma variável de array não é apenas "realmente" um ponteiro para o primeiro elemento, é um array. Arrays não são ponteiros.
GManNickG
Obrigado pelo comentário, mas quando leio aquela seção do artigo ele diz de cara que os arrays não sabem seu próprio tamanho, então usa um exemplo em que os arrays são passados ​​como argumentos para ilustrar esse fato. Portanto, quando os arrays são passados ​​como argumentos, eles perderam as informações sobre seu tamanho ou nunca tiveram as informações para começar. Eu presumi o último.
Scott Turley
3
O compilador pode dizer a diferença entre duas matrizes de tamanhos diferentes - tente imprimir sizeof(a)vs. sizeof(b)ou passar apara void f(int (&)[20]);.
Georg Fritzsche
É importante entender que cada tamanho de array constitui seu próprio tipo. As regras para passagem de parâmetro garantem que você possa escrever funções "genéricas" do pobre que usam argumentos de array de qualquer tamanho, ao custo de precisar passar o tamanho separadamente. Se não fosse esse o caso (e em C ++ você pode - e deve! - definir parâmetros de referência para matrizes de tamanho específico), você precisaria de uma função específica para cada tamanho diferente, claramente um absurdo. Eu escrevi sobre isso em outro post .
Peter - Reintegrar Monica de
0

Eu sei, todos os que responderam são especialistas em C / C ++. Mas pensei, esta é a razão principal.

num2 = num1;

Aqui você está tentando alterar o endereço base da matriz, o que não é permitido.

e, claro, struct2 = struct1;

Aqui, o objeto struct1 é atribuído a outro objeto.

nsivakr
fonte
E atribuir structs acabará por atribuir o membro da matriz, o que levanta a mesma questão. Por que um é permitido e não o outro, quando é um array nas duas situações?
GManNickG de
1
Acordado. Mas o primeiro é impedido pelo compilador (num2 = num1). O segundo não é impedido pelo compilador. Isso faz uma grande diferença.
nsivakr
Se as matrizes fossem atribuíveis, num2 = num1seria perfeitamente bem comportado. Os elementos de num2teriam o mesmo valor do elemento correspondente de num1.
juanchopanza
0

Outra razão pela qual nenhum esforço adicional foi feito para reforçar arrays em C é provavelmente que a atribuição de array não seria tão útil. Embora possa ser facilmente alcançado em C, envolvendo-o em uma estrutura (e o endereço da estrutura pode ser simplesmente convertido para o endereço da matriz ou até mesmo o endereço do primeiro elemento da matriz para processamento posterior), esse recurso raramente é usado. Uma razão é que os arrays de tamanhos diferentes são incompatíveis, o que limita os benefícios da atribuição ou, relacionado, a passagem para funções por valor.

A maioria das funções com parâmetros de array em linguagens em que arrays são tipos de primeira classe são escritos para arrays de tamanho arbitrário. A função, então, geralmente itera sobre um determinado número de elementos, uma informação que o array fornece. (Em C, o idioma é, obviamente, passar um ponteiro e uma contagem de elemento separada.) Uma função que aceita uma matriz de apenas um tamanho específico não é necessária com tanta frequência, portanto, não é esquecido muito. (Isso muda quando você pode deixar que o compilador gere uma função separada para qualquer tamanho de array que ocorra, como acontece com os modelos C ++; este é o motivo pelo qual std::arrayé útil.)

Peter - Reintegrar Monica
fonte