Primeiro, precisamos voltar ao que significa passar por valor e por referência.
Para linguagens como Java e SML, a passagem por valor é direta (e não há passagem por referência), assim como a cópia de um valor variável é, pois todas as variáveis são apenas escalares e possuem cópia semântica: elas são o que contam como aritmética digite C ++ ou "referências" (ponteiros com nome e sintaxe diferentes).
Em C, temos tipos escalares e definidos pelo usuário:
- Os escalares têm um valor numérico ou abstrato (ponteiros não são números, eles têm um valor abstrato) que é copiado.
- Os tipos agregados têm todos os seus membros possivelmente inicializados copiados:
- para tipos de produtos (matrizes e estruturas): recursivamente, todos os membros de estruturas e elementos de matrizes são copiados (a sintaxe da função C não permite passar matrizes por valor diretamente, apenas matrizes membros de uma estrutura, mas isso é um detalhe )
- para tipos de soma (uniões): o valor do "membro ativo" é preservado; obviamente, a cópia de membro por membro não está em ordem, pois nem todos os membros podem ser inicializados.
No C ++, os tipos definidos pelo usuário podem ter semântica de cópia definida pelo usuário, o que permite uma programação verdadeiramente "orientada a objetos" com objetos com propriedade de seus recursos e operações de "cópia profunda". Nesse caso, uma operação de cópia é realmente uma chamada para uma função que quase pode executar operações arbitrárias.
Para estruturas C compiladas como C ++, "copiar" ainda é definido como chamar a operação de cópia definida pelo usuário (construtor ou operador de atribuição), que são gerados implicitamente pelo compilador. Isso significa que a semântica de um programa de subconjunto comum de C / C ++ é diferente em C e C ++: em C, todo um tipo de agregado é copiado; em C ++, uma função de cópia gerada implicitamente é chamada para copiar cada membro; o resultado final é que, em ambos os casos, cada membro é copiado.
(Acho que há uma exceção quando uma estrutura dentro de uma união é copiada.)
Portanto, para um tipo de classe, a única maneira (fora da união de cópias) de criar uma nova instância é através de um construtor (mesmo para aqueles com construtores triviais gerados por compiladores).
Você não pode pegar o endereço de um rvalue por meio de um operador unário, &
mas isso não significa que não há objeto rvalue; e um objeto, por definição, tem um endereço ; e esse endereço é mesmo representado por uma construção de sintaxe: um objeto do tipo classe só pode ser criado por um construtor e possui um this
ponteiro; mas para tipos triviais, não há construtor gravado pelo usuário; portanto, não há lugar para colocar this
até que a cópia seja construída e nomeada.
Para o tipo escalar, o valor de um objeto é o rvalor do objeto, o valor matemático puro armazenado no objeto.
Para um tipo de classe, a única noção de um valor do objeto é outra cópia do objeto, que só pode ser feita por um construtor de cópias, uma função real (embora, para tipos triviais, essa função seja tão trivial, às vezes isso pode ser criado sem chamar o construtor). Isso significa que o valor do objeto é o resultado da alteração do estado do programa global por uma execução . Não acessa matematicamente.
Portanto, passar por valor realmente não é uma coisa: é passar por chamada de construtor de cópia , o que é menos bonito. Espera-se que o construtor de cópia execute uma operação sensata de "cópia" de acordo com a semântica apropriada do tipo de objeto, respeitando seus invariantes internos (que são propriedades abstratas do usuário, não propriedades intrínsecas do C ++).
Passar pelo valor de um objeto de classe significa:
- crie outra instância
- faça com que a função chamada atue nessa instância.
Observe que o problema não tem nada a ver com a cópia em si ser um objeto com um endereço: todos os parâmetros de função são objetos e têm um endereço (no nível semântico do idioma).
A questão é se:
- a cópia é um novo objeto inicializado com o valor matemático puro (valor puro verdadeiro) do objeto original, como nos escalares;
- ou a cópia é o valor do objeto original , como nas classes.
No caso de um tipo de classe trivial, você ainda pode definir o membro da cópia de membro do original, para definir o valor puro do original devido à trivialidade das operações de cópia (construtor e atribuição de cópia). Não é assim com funções especiais arbitrárias do usuário: um valor do original deve ser uma cópia construída.
Objetos de classe devem ser construídos pelo chamador; um construtor formalmente tem um this
ponteiro, mas o formalismo não é relevante aqui: todos os objetos têm formalmente um endereço, mas somente aqueles que realmente usam seu endereço de maneiras não puramente locais (ao contrário do *&i = 1;
que é puramente o uso local de endereço) precisam ter um bem definido endereço.
Um objeto deve absolutamente passar por endereço se parecer que ele possui um endereço nessas duas funções compiladas separadamente:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
Aqui, mesmo que something(address)
seja uma função ou macro pura ou qualquer outra coisa (como printf("%p",arg)
) que não possa armazenar o endereço ou se comunicar com outra entidade, temos o requisito de passar por endereço, porque o endereço deve ser bem definido para um objeto único int
que possui um único identidade.
Não sabemos se uma função externa será "pura" em termos de endereços passados para ela.
Aqui, o potencial para um uso real do endereço em um construtor ou destruidor não trivial do lado do chamador é provavelmente o motivo para seguir a rota segura e simplista e dar ao objeto uma identidade no chamador e transmitir seu endereço, conforme ele faz Certifique-se de que qualquer uso não trivial de seu endereço no construtor, após a construção e no destruidor seja consistente : this
deve parecer o mesmo sobre a existência do objeto.
Um construtor ou destruidor não trivial, como qualquer outra função, pode usar o this
ponteiro de uma maneira que exija consistência sobre seu valor, mesmo que algum objeto com material não trivial possa não:
struct file_handler { // don't use that class!
file_handler () { this->fileno = -1; }
file_handler (int f) { this->fileno = f; }
file_handler (const file_handler& rhs) {
if (this->fileno != -1)
this->fileno = dup(rhs.fileno);
else
this->fileno = -1;
}
~file_handler () {
if (this->fileno != -1)
close(this->fileno);
}
file_handler &operator= (const file_handler& rhs);
};
Observe que, nesse caso, apesar do uso explícito de um ponteiro (sintaxe explícita this->
), a identidade do objeto é irrelevante: o compilador pode muito bem usar copiar bit a bit o objeto para movê-lo e fazer "copiar elisão". Isso se baseia no nível de "pureza" do uso de this
funções-membro especiais (o endereço não escapa).
Mas pureza não é um atributo disponível no nível de declaração padrão (existem extensões do compilador que adicionam descrição de pureza a declarações de funções não embutidas), portanto, você não pode definir uma ABI com base na pureza do código que pode não estar disponível (o código pode ou não pode não estar em linha e disponível para análise).
A pureza é medida como "certamente pura" ou "impura ou desconhecida". O terreno comum, ou o limite superior da semântica (na verdade, máximo), ou LCM (Mínimo Múltiplo Comum) é "desconhecido". Assim, a ABI resolve o desconhecido.
Resumo:
- Algumas construções requerem que o compilador defina a identidade do objeto.
- O ABI é definido em termos de classes de programas e não em casos específicos que podem ser otimizados.
Possível trabalho futuro:
A anotação de pureza é útil o suficiente para ser generalizada e padronizada?