Primero necesitamos volver a lo que significa pasar por valor y por referencia.
Para lenguajes como Java y SML, el paso por valor es sencillo (y no hay paso por referencia), al igual que copiar un valor variable, ya que todas las variables son solo escalares y tienen una copia semántica incorporada: son lo que cuentan como aritmética escriba C ++ o "referencias" (punteros con diferentes nombres y sintaxis).
En C tenemos tipos escalares y definidos por el usuario:
- Los escalares tienen un valor numérico o abstracto (los punteros no son números, tienen un valor abstracto) que se copia.
- Los tipos agregados tienen todos sus miembros posiblemente inicializados copiados:
- para tipos de productos (matrices y estructuras): de forma recursiva, todos los miembros de estructuras y elementos de matrices se copian (la sintaxis de la función C no permite pasar matrices por valor directamente, solo las matrices miembros de una estructura, pero eso es un detalle )
- para tipos de suma (uniones): se conserva el valor del "miembro activo"; obviamente, la copia miembro por miembro no está en orden, ya que no todos los miembros pueden inicializarse.
En C ++, los tipos definidos por el usuario pueden tener una semántica de copia definida por el usuario, que permite una programación verdaderamente "orientada a objetos" con objetos con propiedad de sus recursos y operaciones de "copia profunda". En tal caso, una operación de copia es realmente una llamada a una función que casi puede realizar operaciones arbitrarias.
Para estructuras C compiladas como C ++, "copiar" todavía se define como llamar a la operación de copia definida por el usuario (ya sea constructor u operador de asignación), que el compilador genera implícitamente. Significa que la semántica de un programa de subconjunto común de C / C ++ es diferente en C y C ++: en C se copia un tipo de agregado completo, en C ++ se llama a una función de copia generada implícitamente para copiar cada miembro; El resultado final es que en cualquier caso se copia cada miembro.
(Creo que hay una excepción cuando se copia una estructura dentro de una unión).
Entonces, para un tipo de clase, la única forma (fuera de las copias de la unión) para hacer una nueva instancia es a través de un constructor (incluso para aquellos con constructores triviales generados por el compilador).
No puede tomar la dirección de un valor r mediante un operador unario, &
pero eso no significa que no haya un objeto rvalue; y un objeto, por definición, tiene una dirección ; y esa dirección incluso está representada por una construcción de sintaxis: un objeto de tipo de clase solo puede ser creado por un constructor, y tiene un this
puntero; pero para los tipos triviales, no hay un constructor escrito por el usuario, por lo que no hay lugar para colocar this
hasta después de que se construye y se nombra la copia.
Para el tipo escalar, el valor de un objeto es el valor r del objeto, el valor matemático puro almacenado en el objeto.
Para un tipo de clase, la única noción de un valor del objeto es otra copia del objeto, que solo puede ser realizada por un constructor de copia, una función real (aunque para tipos triviales esa función es especialmente trivial, a veces puede ser creado sin llamar al constructor). Eso significa que el valor del objeto es el resultado del cambio del estado global del programa por una ejecución . No accede matemáticamente.
Por lo tanto, pasar por valor realmente no es una cosa: es pasar por llamada de constructor de copia , que es menos bonita. Se espera que el constructor de copia realice una operación de "copia" sensata de acuerdo con la semántica adecuada del tipo de objeto, respetando sus invariantes internos (que son propiedades abstractas del usuario, no propiedades intrínsecas de C ++).
Pasar por valor de un objeto de clase significa:
- crear otra instancia
- luego haga que la función llamada actúe en esa instancia.
Tenga en cuenta que el problema no tiene nada que ver con si la copia en sí es un objeto con una dirección: todos los parámetros de función son objetos y tienen una dirección (en el nivel semántico del lenguaje).
El problema es si:
- la copia es un nuevo objeto inicializado con el valor matemático puro (verdadero valor puro) del objeto original, como con los escalares;
- o la copia es el valor del objeto original , como con las clases.
En el caso de un tipo de clase trivial, aún puede definir el miembro de la copia miembro del original, por lo que puede definir el valor puro del original debido a la trivialidad de las operaciones de copia (constructor de copia y asignación). No es así con funciones de usuario especiales arbitrarias: un valor del original tiene que ser una copia construida.
Los objetos de clase deben ser construidos por la persona que llama; un constructor formalmente tiene un this
puntero, pero el formalismo no es relevante aquí: todos los objetos tienen formalmente una dirección, pero solo aquellos que realmente usan su dirección de manera no puramente local (a diferencia de lo *&i = 1;
que es el uso puramente local de la dirección) deben tener un bien definido habla a.
Un objeto debe pasar absolutamente por dirección si parece tener una dirección en estas dos funciones compiladas por separado:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
Aquí, incluso si se something(address)
trata de una función pura o macro o lo que sea (como printf("%p",arg)
) que no puede almacenar la dirección o comunicarse con otra entidad, tenemos el requisito de pasar por dirección porque la dirección debe estar bien definida para un objeto único int
que tiene un único identidad.
No sabemos si una función externa será "pura" en términos de direcciones que se le pasen.
Aquí, el potencial para un uso real de la dirección en un constructor o destructor no trivial en el lado de la persona que llama es probablemente la razón para tomar la ruta segura y simplista y darle al objeto una identidad en la persona que llama y pasar su dirección, ya que hace asegúrese de que cualquier uso no trivial de su dirección en el constructor, después de la construcción y en el destructor sea consistente : this
debe parecer ser el mismo sobre la existencia del objeto.
Un constructor o destructor no trivial como cualquier otra función puede usar el this
puntero de una manera que requiera consistencia sobre su valor a pesar de que algún objeto con cosas no triviales no:
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);
};
Tenga en cuenta que en ese caso, a pesar del uso explícito de un puntero (sintaxis explícita this->
), la identidad del objeto es irrelevante: el compilador bien podría usar copiar bit a bit el objeto para moverlo y hacer "copiar elisión". Esto se basa en el nivel de "pureza" del uso de this
funciones miembro especiales (la dirección no se escapa).
Pero la pureza no es un atributo disponible en el nivel de declaración estándar (existen extensiones del compilador que agregan una descripción de pureza en la declaración de función no en línea), por lo que no puede definir un ABI basado en la pureza del código que puede no estar disponible (el código puede o puede no estar en línea y disponible para análisis).
La pureza se mide como "ciertamente pura" o "impura o desconocida". El terreno común, o límite superior de la semántica (en realidad el máximo), o LCM (mínimo común múltiplo) es "desconocido". Entonces el ABI se decide por lo desconocido.
Resumen:
- Algunas construcciones requieren que el compilador defina la identidad del objeto.
- El ABI se define en términos de clases de programas y no en casos específicos que podrían optimizarse.
Posible trabajo futuro:
¿Es la anotación de pureza lo suficientemente útil como para ser generalizada y estandarizada?