sobrecarga sinonimo redaccion que operadores operador nominal musica linguistica ingles fonemas elision elipsis ejercicios ejemplos copia constructores consonantes consiste con clases asimilacion asignacion argumentos adiciones adicion c++ language-lawyer c++17

c++ - sinonimo - Con elision de copia garantizada, ¿por qué la clase necesita estar completamente definida?



sobrecarga de operadores c++ pdf (4)

A pesar de la cantidad de respuestas y la cantidad de comentarios publicados en este hilo (que ha respondido a todas mis preguntas personales), he decidido publicar una respuesta "para el resto de nosotros". Al principio no entendía a qué se refería el OP, pero ahora lo hago, así que pensé en compartirlo. Si sabe todo esto y le aburre, querido lector, por favor, siga adelante.

@xskxzr y @hvd respondieron la pregunta de manera efectiva, pero la publicación de @ hvd está especialmente en standardese y asume que los lectores saben cómo funciona el retorno por valor (y por extensión, RVO ), lo cual me imagino que no todos lo hacen. Pensé que sí, pero me faltaba un detalle importante (que, cuando lo piensas, es bastante obvio, pero aún así, me lo perdí).

Entonces, esta publicación se enfoca principalmente en eso, para que todos podamos ver por qué (a) el OP se preguntó por qué había una bar() compilación bar() en primer lugar, y luego (b) se dio cuenta de la razón.

Entonces, miremos ese código otra vez. Dado esto (que es legal, incluso con un tipo definido por completo):

class C; C foo();

¿Por qué el compilador no puede compilar esto (he eliminado el inline porque es irrelevante):

C bar() { return foo(); }

El mensaje de error de gcc es:

error: el tipo de retorno ''clase C'' está incompleto

Bueno, primero, la respuesta aceptada cita el párrafo relevante de la norma que lo prohíbe explícitamente, por lo que no hay misterio allí. Pero el OP (y, de hecho, el comentarista Walter, quien se dio cuenta de esto de inmediato) quería saber por qué.

Bueno, al principio me pareció obvio: el llamador debe asignar espacio para el resultado de la función y no sabe qué tan grande es el objeto, por lo que el compilador está en un quandry. Pero me faltaba un truco, y eso radica en la forma en que funciona el retorno por valor.

Ahora, para aquellos que no saben, devolver objetos de clase por valor funciona mediante la asignación de espacio para el objeto devuelto en la pila y pasarle un puntero como un parámetro oculto a la función que se llama, que luego construye el objeto, manipula lo que sea

Sin embargo , esto encadena, así que si tenemos el siguiente código (donde definimos completamente C antes de llamar a la bar() ):

class C { public: int x; }; C c = bar (); c.x = 4;

luego, el espacio para c se asigna antes de llamar a la bar() y la dirección de c se pasa como un parámetro oculto a la bar() , y luego se pasa directamente a foo() , que finalmente llena el objeto en la ubicación deseada. Entonces, debido a que bar() realidad no hizo nada con este puntero (aparte de pasarlo), entonces todo lo que le importa es el puntero en sí, y no lo que apunta.

O lo hace? Bueno, en realidad, sí, lo hace.

Cuando se devuelve un objeto de clase por valor, los objetos pequeños generalmente se devuelven en un registro (o un par de registros) como una optimización. El compilador puede hacer esto en la mayoría de los casos donde el objeto es lo suficientemente pequeño (más sobre esto en un momento).

Pero ahora, bar() necesita saber si esto es lo que va a hacer foo() , y para hacerlo necesita, por varias razones, ver la declaración completa de la clase.

Entonces, en resumen, es por eso que el compilador necesita un tipo completamente definido para llamar a foo() , de lo contrario no sabrá qué espera foo () y por lo tanto no sabe qué código generar. No en la mayoría de las plataformas, al final de la historia.

Notas:

  1. Miré a gcc y parece que hay dos reglas (completamente lógicas) para determinar si un objeto de clase se devuelve en un registro o en un par de registros:

    • a) El objeto es de 16 bytes o más pequeño (en una compilación de 64 bits).
    • b) std::is_trivially_copyable<T>::value evalúa como true (quizás alguien pueda encontrar algo en el estándar al respecto).
  2. En caso de que algún lector no sepa, RVO se basa en construir el objeto en su lugar de descanso final (es decir, en la ubicación asignada por el llamante). Esto se debe a que hay objetos (como algunas implementaciones de std::basic_string , creo) que son sensibles a moverse en la memoria, por lo que no puedes construirlos en un lugar conveniente para ti y luego memcpy en otro lugar.

  3. Si no es posible construir el objeto devuelto en esa ubicación final (debido a la forma en que codificó la función que devuelve el objeto), entonces no ocurre RVO (¿cómo puede hacerlo?), Vea la demostración en vivo a continuación ( make_no_RVO() ).

  4. Como ejemplo específico del punto 1b, si un objeto pequeño contiene miembros de datos que (podrían) señalarse a sí mismo o a cualquiera de sus miembros de datos, devolverlo por valor lo pondrá en problemas si no lo declara correctamente. Solo con agregar un constructor de copia vacío, ya no se podrá copiar de forma trivial. Pero supongo que eso es cierto en general, no oculte información importante del compilador.

Demo en vivo here . Todos los comentarios en esta publicación son bienvenidos, los responderé lo mejor que pueda.

Un seguimiento a este post . Considera lo siguiente:

class C; C foo();

Eso es un par de declaraciones válidas. C no necesita estar completamente definido cuando simplemente declara una función. Pero si tuviéramos que añadir la siguiente función:

class C; C foo(); inline C bar() { return foo(); }

Entonces, de repente, C necesita ser un tipo completamente definido. Pero con elision de copia garantizada, ninguno de sus miembros es requerido. No hay copia ni movimiento, el valor se inicializa en otro lugar y se destruye solo en el contexto de la persona que llama ( bar ).

¿Entonces por qué? ¿Qué en la norma lo prohíbe?


Elección de copia garantizada tiene excepciones, por razones de compatibilidad y / o eficiencia. Los tipos que se pueden copiar de forma trivial pueden copiarse incluso cuando, de lo contrario, se garantizaría la elección de la copia. Tienes razón en que si esto no se aplica, entonces el compilador podría generar el código correcto sin conocer ningún detalle de C , ni siquiera su tamaño. Pero el compilador necesita saber si esto aplica, y para eso, todavía necesita que el tipo esté completo.

De acuerdo con https://timsong-cpp.github.io/cppwp/class.temporary :

15.2 Objetos temporales [class.temporary]

1 objetos temporales son creados

[...]

(1.2): cuando la implementación lo requiera para pasar o devolver un objeto de tipo de copia trivial (ver más abajo), y

[...]

3 Cuando un objeto de clase de clase X se pasa o se devuelve desde una función, si cada constructor de copia, mover constructor y destructor de X es trivial o eliminado, y X tiene al menos una copia o constructor de movimiento no eliminado, implementaciones se les permite crear un objeto temporal para contener el parámetro de función u objeto de resultado. El objeto temporal se construye a partir del argumento de la función o el valor de retorno, respectivamente, y el parámetro de la función o el objeto de retorno se inicializa como si se usara el constructor trivial no eliminado para copiar el temporal (incluso si ese constructor es inaccesible o no se seleccionaría por resolución de sobrecarga para realizar una copia o movimiento del objeto). [ Nota: esta latitud se otorga para permitir que los objetos de tipo de clase se pasen o se devuelvan desde las funciones de los registros. - nota final ]


Esto no tiene nada que ver con ellision de copia. se supone que foo devuelve un valor de C Siempre que pase una referencia o un puntero a foo , está bien. Una vez que intentes llamar a foo , como es el caso en la bar , el tamaño de sus argumentos y el valor de retorno deben estar disponibles; La única forma válida de saber que está presentando una declaración completa del tipo requerido. Si la firma hubiera estado usando una referencia o un puntero, toda la información requerida estaba presente y usted podría prescindir de la declaración de tipo completo. Este enfoque tiene un nombre: pimpl == Puntero a IMPLementaion, y se usa ampliamente como un medio para ocultar detalles en las distribuciones de bibliotecas de código cerrado.


La regla radica en [basic.lval]/9 :

A menos que se indique lo contrario ([dcl.type.simple]), un prvalue siempre tendrá el tipo completo o el tipo vacío. ...