¿Es una mala práctica escribir código que se base en optimizaciones del compilador?

99

He estado aprendiendo algo de C ++, y a menudo tengo que devolver objetos grandes de funciones que se crean dentro de la función. Sé que existe el paso por referencia, devolver un puntero y devolver soluciones de tipo de referencia, pero también he leído que los compiladores de C ++ (y el estándar C ++) permiten la optimización del valor de retorno, lo que evita copiar estos objetos grandes a través de la memoria, por lo tanto ahorrando el tiempo y la memoria de todo eso.

Ahora, siento que la sintaxis es mucho más clara cuando el valor devuelve explícitamente el objeto, y el compilador generalmente empleará el RVO y hará que el proceso sea más eficiente. ¿Es una mala práctica confiar en esta optimización? Hace que el código sea más claro y más legible para el usuario, lo cual es extremadamente importante, pero ¿debo tener cuidado de asumir que el compilador aprovechará la oportunidad de RVO?

¿Es esto una microoptimización o algo que debo tener en cuenta al diseñar mi código?

Mate
fuente
77
Para responder a su edición, es una micro optimización porque incluso si intentara comparar lo que está ganando en nanosegundos, apenas lo vería. Por lo demás, estoy demasiado podrido en C ++ para proporcionarle una respuesta estricta de por qué no funcionaría. Uno de ellos es probable que haya casos en los que necesite una asignación dinámica y, por lo tanto, use new / pointer / references.
Walfrat
44
@Walfrat incluso si los objetos son bastante grandes, del orden de megabytes? Mis matrices pueden ser enormes debido a la naturaleza de los problemas que estoy resolviendo.
Matt
66
@ Matt no lo haría. Las referencias / punteros existen precisamente para esto. Se supone que las optimizaciones del compilador van más allá de lo que los programadores deben tener en cuenta al construir un programa, aunque sí, a menudo los dos mundos se superponen.
Neil
55
@Matt A menos que esté haciendo algo extremadamente específico que suponga requerir desarrolladores con más de 10 años de experiencia en C / kernels, bajas interacciones de hardware, no debería necesitar eso. Si crees que perteneces a algo muy específico, edita tu publicación y agrega una descripción precisa de lo que se supone que debe hacer tu aplicación (¿cálculo matemático pesado en tiempo real? ...)
Walfrat
37
En el caso particular de R ++ de C ++ (N), sí, confiar en esta optimización es perfectamente válido. Esto se debe a que el estándar C ++ 17 exige específicamente que suceda, en las situaciones en que los compiladores modernos ya lo estaban haciendo.
Caleth

Respuestas:

130

Emplea el principio de menos asombro .

¿Eres tú y solo tú quien usará este código, y estás seguro de que lo mismo que tú en 3 años no te sorprenderá con lo que haces?

Entonces adelante.

En todos los demás casos, use la forma estándar; de lo contrario, usted y sus colegas se encontrarán con errores difíciles de encontrar.

Por ejemplo, mi colega se quejaba de que mi código causaba errores. Resulta que había desactivado la evaluación booleana de corto circuito en la configuración de su compilador. Casi lo abofeteo.

Pieter B
fuente
88
@Neil, ese es mi punto, todos confían en la evaluación de cortocircuitos. Y no debería tener que pensarlo dos veces, debería estar encendido. Es un estándar de facto. Sí, puedes cambiarlo, pero no deberías.
Pieter B
49
"¡Cambié la forma en que funciona el idioma, y tu sucio código podrido se rompió! ¡Arghh!" Guau. Sería apropiado abofetear, envíe a su colega a un entrenamiento Zen, hay mucho de eso allí.
109
@PieterB Estoy bastante seguro de que las especificaciones de lenguaje C y C ++ garantizan la evaluación de cortocircuitos. Entonces no es solo un estándar de facto, es el estándar. Sin él, ya ni siquiera estás usando C / C ++, sino algo sospechosamente parecido: P
marcelm
47
Solo como referencia, la forma estándar aquí es regresar por valor.
DeadMG
28
@ dan04 sí, fue en Delphi. Chicos, no se dejen atrapar por el ejemplo que se trata del punto que hice. No hagas cosas sorprendentes que nadie más haga.
Pieter B
81

Para este caso en particular, definitivamente solo regrese por valor.

  • RVO y NRVO son optimizaciones bien conocidas y robustas que realmente deberían ser realizadas por cualquier compilador decente, incluso en modo C ++ 03.

  • La semántica de movimiento asegura que los objetos se muevan fuera de las funciones si (N) RVO no tuvo lugar. Eso solo es útil si su objeto usa datos dinámicos internamente (como lo std::vectorhace), pero ese debería ser el caso si es tan grande: desbordar la pila es un riesgo con grandes objetos automáticos.

  • C ++ 17 impone RVO. Así que no te preocupes, no desaparecerá en ti y solo terminará de establecerse por completo una vez que los compiladores estén actualizados.

Y al final, forzar una asignación dinámica adicional para que devuelva un puntero, o forzar que su tipo de resultado sea construible por defecto solo para que pueda pasarlo como un parámetro de salida son soluciones feas y no idiomáticas a un problema que probablemente nunca tendrá tener.

Simplemente escriba código que tenga sentido y agradezca a los escritores del compilador por optimizar correctamente el código que tiene sentido.

Quentin
fuente
99
Solo por diversión, vea cómo Borland Turbo C ++ 3.0 de 1990-ish maneja RVO . Spoiler: Básicamente funciona bien.
nwp
99
La clave aquí es que no es una optimización aleatoria específica del compilador o una "característica no documentada", sino algo que, aunque técnicamente es opcional en varias versiones del estándar C ++, fue fuertemente impulsado por la industria y casi todos los compiladores principales lo han hecho por Un largo tiempo.
77
Esta optimización no es tan robusta como uno quisiera. Sí, es bastante confiable en los casos más obvios, pero observando, por ejemplo, el bugzilla de gcc, hay muchos casos apenas menos obvios en los que se pasa por alto.
Marc Glisse
62

Ahora, siento que la sintaxis es mucho más clara cuando el valor devuelve explícitamente el objeto, y el compilador generalmente empleará el RVO y hará que el proceso sea más eficiente. ¿Es una mala práctica confiar en esta optimización? Hace que el código sea más claro y más legible para el usuario, lo cual es extremadamente importante, pero ¿debo tener cuidado de asumir que el compilador aprovechará la oportunidad de RVO?

Esta no es una micro-optimización poco conocida, cursi, de la que lees en algún blog pequeño y poco traficado, y luego te sientes inteligente y superior sobre el uso.

Después de C ++ 11, RVO es la forma estándar de escribir este código de código. Es común, esperado, enseñado, mencionado en charlas, mencionado en blogs, mencionado en el estándar, será reportado como un error del compilador si no se implementa. En C ++ 17, el lenguaje va un paso más allá y obliga a copiar la elisión en ciertos escenarios.

Debe confiar absolutamente en esta optimización.

Además de eso, el retorno por valor solo conduce a un código enormemente más fácil de leer y administrar que el código que se devuelve por referencia. La semántica de valor es una cosa poderosa, que en sí misma podría conducir a más oportunidades de optimización.

Barry
fuente
3
Gracias, esto tiene mucho sentido y es consistente con el "principio de menos asombro" mencionado anteriormente. Haría que el código fuera muy claro y comprensible, y hace que sea más difícil meterse con travesuras de puntero.
Matt
3
@Matt Parte de la razón por la que voté por esta respuesta es que menciona la "semántica de valor". A medida que obtenga más experiencia en C ++ (y programación en general), encontrará situaciones ocasionales en las que la semántica de valores no se puede usar para ciertos objetos porque son mutables y sus cambios deben hacerse visibles para otro código que use ese mismo objeto (un ejemplo de "mutabilidad compartida"). Cuando ocurren estas situaciones, los objetos afectados deberán compartirse a través de punteros (inteligentes).
rwong
16

La exactitud del código que escribe nunca debe depender de una optimización. Debe generar el resultado correcto cuando se ejecuta en la "máquina virtual" de C ++ que utilizan en la especificación.

Sin embargo, de lo que habla es más una cuestión de eficiencia. Su código funciona mejor si está optimizado con un compilador de optimización RVO. Eso está bien, por todas las razones señaladas en las otras respuestas.

Sin embargo, si necesita esta optimización (por ejemplo, si el constructor de la copia realmente haría que su código fallara), ahora está a gusto del compilador.

Creo que el mejor ejemplo de esto en mi propia práctica es la optimización de llamadas de cola:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

Es un ejemplo tonto, pero muestra una llamada de cola, donde una función se llama recursivamente justo al final de una función. La máquina virtual C ++ mostrará que este código funciona correctamente, aunque puedo causar un poco de confusión en cuanto a por qué me molesté en escribir una rutina de suma en primer lugar. Sin embargo, en implementaciones prácticas de C ++, tenemos una pila y tiene un espacio limitado. Si se realiza de forma pedante, esta función debería empujar al menos b + 1los cuadros de la pila a la pila a medida que se agrega. Si quiero calcular sillyAdd(5, 7), esto no es gran cosa. Si quiero calcular sillyAdd(0, 1000000000), podría tener problemas reales de causar un StackOverflow (y no del tipo bueno ).

Sin embargo, podemos ver que una vez que llegamos a la última línea de retorno, realmente hemos terminado con todo en el marco de la pila actual. Realmente no necesitamos mantenerlo ahí. La optimización de llamadas de cola le permite "reutilizar" el marco de pila existente para la siguiente función. De esta manera, solo necesitamos 1 marco de pila, en lugar de b+1. (Todavía tenemos que hacer todas esas adiciones y restas tontas, pero no ocupan más espacio). En efecto, la optimización convierte el código en:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

En algunos idiomas, la especificación requiere explícitamente la optimización de las llamadas de cola. C ++ no es uno de esos. No puedo confiar en los compiladores de C ++ para reconocer esta oportunidad de optimización de llamadas de cola, a menos que vaya caso por caso. Con mi versión de Visual Studio, la versión de lanzamiento optimiza las llamadas de cola, pero la versión de depuración no (por diseño).

Por lo tanto, sería malo para mí depender de poder calcular sillyAdd(0, 1000000000).

Cort Ammon
fuente
2
Este es un caso interesante, pero no creo que pueda generalizarlo a la regla en su primer párrafo. Supongamos que tengo un programa para un dispositivo pequeño, que se cargará si y solo si uso las optimizaciones de reducción de tamaño del compilador, ¿es incorrecto hacerlo? Parece bastante pedante decir que mi única opción válida es reescribirlo en ensamblador, especialmente si esa reescritura hace lo mismo que hace el optimizador para resolver el problema.
sdenham
55
@ Sdenham Supongo que hay un pequeño espacio en la discusión. Si ya no escribe para "C ++", sino para "Compilador WindRiver C ++ versión 3.4.1", entonces puedo ver la lógica allí. Sin embargo, como regla general, si está escribiendo algo que no funciona correctamente de acuerdo con las especificaciones, se encuentra en un tipo de escenario muy diferente. Sé que la biblioteca Boost tiene un código como ese, pero siempre lo ponen en #ifdefbloques y tienen disponible una solución alternativa que cumple con los estándares.
Cort Ammon
44
¿Es eso un error tipográfico en el segundo bloque de código donde dice b = b + 1?
stib
2
Es posible que desee explicar qué quiere decir con "máquina virtual C ++", ya que ese no es un término utilizado en ningún documento estándar. Yo creo que estamos hablando del modelo de ejecución de C ++, pero no del todo cierto - y su término es engañosamente similar a una "máquina virtual de código de bytes", que se refiere a algo totalmente diferente.
Toby Speight
1
@supercat Scala también tiene una sintaxis de recursión de cola explícita. C ++ es su propia bestia, pero creo que la recursión de la cola es unidiomática para los lenguajes no funcionales, y obligatoria para los lenguajes funcionales, dejando un pequeño conjunto de lenguajes donde es razonable tener una sintaxis de recursión de cola explícita. Traducir literalmente la recursión de la cola a bucles y mutación explícita simplemente es una mejor opción para muchos idiomas.
prosfilaes
8

En la práctica, los programas de C ++ esperan algunas optimizaciones del compilador.

Mire notablemente los encabezados estándar de sus implementaciones de contenedores estándar . Con GCC , puede solicitar el formulario preprocesado ( g++ -C -E) y la representación interna de GIMPLE ( g++ -fdump-tree-gimpleo Gimple SSA con -fdump-tree-ssa) de la mayoría de los archivos fuente (unidades de traducción técnica) utilizando contenedores. Te sorprenderá la cantidad de optimización que se realiza (con g++ -O2). Por lo tanto, los implementadores de contenedores confían en las optimizaciones (y la mayoría de las veces, el implementador de una biblioteca estándar de C ++ sabe qué optimización sucedería y escribe la implementación del contenedor con esos en mente; a veces también escribía el pase de optimización en el compilador para lidiar con las características requeridas por la biblioteca estándar de C ++).

En la práctica, son las optimizaciones del compilador las que hacen que C ++ y sus contenedores estándar sean lo suficientemente eficientes. Entonces puedes confiar en ellos.

Y del mismo modo para el caso de RVO mencionado en su pregunta.

El estándar C ++ fue codiseñado (especialmente experimentando optimizaciones lo suficientemente buenas al proponer nuevas características) para funcionar bien con las posibles optimizaciones.

Por ejemplo, considere el siguiente programa:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

compilarlo con g++ -O3 -fverbose-asm -S. Descubrirá que la función generada no ejecuta ninguna CALLinstrucción de máquina. Por lo tanto, la mayoría de los pasos de C ++ (construcción de un cierre lambda, su aplicación repetida, obtener los iteradores beginy end, etc.) se han optimizado. El código de máquina contiene solo un bucle (que no aparece explícitamente en el código fuente). Sin tales optimizaciones, C ++ 11 no tendrá éxito.

adenda

(diciembre añadido 31 st 2017)

Ver CppCon 2017: Matt Godbolt “¿Qué ha hecho mi compilador últimamente por mí? Desatornillar la tapa del compilador ” .

Basile Starynkevitch
fuente
4

Siempre que use un compilador, el entendimiento es que producirá código de máquina o byte para usted. No garantiza nada acerca de cómo es ese código generado, excepto que implementará el código fuente de acuerdo con la especificación del lenguaje. Tenga en cuenta que esta garantía es la misma independientemente del nivel de optimización utilizado, por lo que, en general, no hay razón para considerar una salida como más "correcta" que la otra.

Además, en esos casos, como RVO, donde se especifica en el idioma, parecería inútil hacer todo lo posible para evitar usarlo, especialmente si hace que el código fuente sea más simple.

Se pone mucho esfuerzo en hacer que los compiladores produzcan resultados eficientes, y claramente la intención es que esas capacidades se utilicen.

Puede haber razones para usar código no optimizado (para la depuración, por ejemplo), pero el caso mencionado en esta pregunta no parece ser uno (y si su código falla solo cuando está optimizado, y no es consecuencia de alguna peculiaridad del dispositivo en el que lo está ejecutando, entonces hay un error en alguna parte, y es poco probable que esté en el compilador).

sdenham
fuente
3

Creo que otros cubrieron bien el ángulo específico sobre C ++ y RVO. Aquí hay una respuesta más general:

Cuando se trata de la corrección, no debe confiar en las optimizaciones del compilador ni en el comportamiento específico del compilador en general. Afortunadamente, no parece estar haciendo esto.

Cuando se trata de rendimiento, debe confiar en el comportamiento específico del compilador en general, y en las optimizaciones del compilador en particular. Un compilador compatible con el estándar es libre de compilar su código de la forma que desee, siempre que el código compilado se comporte de acuerdo con las especificaciones del lenguaje. Y no conozco ninguna especificación para un lenguaje convencional que especifique qué tan rápido debe ser cada operación.

svick
fuente
1

Las optimizaciones del compilador solo deberían afectar el rendimiento, no los resultados. Confiar en las optimizaciones del compilador para cumplir con los requisitos no funcionales no solo es razonable, sino que con frecuencia es la razón por la cual un compilador se elige sobre otro.

Las banderas que determinan cómo se realizan operaciones particulares (condiciones de índice o desbordamiento, por ejemplo), a menudo se agrupan con optimizaciones del compilador, pero no deberían. Eficazmente afectan los resultados de los cálculos.

Si la optimización del compilador produce resultados diferentes, eso es un error, un error en el compilador. Confiar en un error en el compilador es a largo plazo un error: ¿qué sucede cuando se soluciona?

El uso de indicadores de compilación que cambian la forma en que funcionan los cálculos debe documentarse bien, pero usarse según sea necesario.

jmoreno
fuente
Desafortunadamente, mucha documentación del compilador hace un mal trabajo al especificar lo que está garantizado o no en varios modos. Además, los escritores de compiladores "modernos" parecen ignorar las combinaciones de garantías que los programadores necesitan y no necesitan. Si un programa funcionaría bien si x*y>zarroja arbitrariamente 0 o 1 en caso de desbordamiento, siempre que no tenga otros efectos secundarios , lo que requiere que un programador evite los desbordamientos a toda costa u obligue al compilador a evaluar la expresión de una manera particular innecesario perjudicar las optimizaciones frente a decir eso ...
supercat
... el compilador podría, en su tiempo libre, comportarse como si x*ypromoviera sus operandos a un tipo arbitrario más largo (permitiendo así formas de elevación y reducción de fuerza que cambiarían el comportamiento de algunos casos de desbordamiento). Sin embargo, muchos compiladores requieren que los programadores eviten el desbordamiento a toda costa o obliguen a los compiladores a truncar todos los valores intermedios en caso de desbordamiento.
supercat
1

No.

Eso es lo que hago todo el tiempo. Si necesito acceder a un bloque arbitrario de 16 bits en la memoria, hago esto

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... y confíe en que el compilador haga todo lo posible para optimizar ese fragmento de código. El código funciona en ARM, i386, AMD64 y prácticamente en todas las arquitecturas existentes. En teoría, un compilador no optimizador podría realmente llamar memcpy, lo que resulta en un rendimiento totalmente malo, pero eso no es un problema para mí, ya que uso optimizaciones de compilador.

Considere la alternativa:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Este código alternativo no funciona en máquinas que requieren una alineación adecuada, si get_pointer()devuelve un puntero no alineado. Además, puede haber problemas de alias en la alternativa.

La diferencia entre -O2 y -O0 cuando se usa el memcpytruco es excelente: 3.2 Gbps de rendimiento de suma de verificación IP versus 67 Gbps de rendimiento de suma de verificación IP. En un orden de diferencia de magnitud!

A veces puede que necesite ayudar al compilador. Entonces, por ejemplo, en lugar de confiar en el compilador para desenrollar bucles, puede hacerlo usted mismo. Ya sea implementando el famoso dispositivo de Duff o de una manera más limpia.

El inconveniente de confiar en las optimizaciones del compilador es que si ejecuta gdb para depurar su código, puede descubrir que se ha optimizado mucho. Por lo tanto, es posible que deba volver a compilar con -O0, lo que significa que el rendimiento será totalmente malo al depurar. Creo que este es un inconveniente que vale la pena tomar, considerando los beneficios de optimizar los compiladores.

Hagas lo que hagas, asegúrate de que tu camino no sea un comportamiento indefinido. Ciertamente, acceder a algún bloque aleatorio de memoria como un entero de 16 bits es un comportamiento indefinido debido a problemas de alias y alineación.

juhist
fuente
0

Todos los intentos de código eficiente escrito en cualquier cosa que no sea ensamblado se basan en gran medida en las optimizaciones del compilador, comenzando con la asignación de registro más básica como eficiente para evitar derrames de pila superfluos en todo el lugar y al menos una selección de instrucciones razonablemente buena, si no excelente. De lo contrario, volveríamos a los años 80, donde tuvimos que poner registerpistas por todas partes y usar el número mínimo de variables en una función para ayudar a los compiladores arcaicos de C o incluso antes, cuando gotoera una optimización de ramificación útil.

Si no pensáramos que podríamos confiar en la capacidad de nuestro optimizador para optimizar nuestro código, todos estaríamos codificando rutas de ejecución críticas para el rendimiento en el ensamblaje.

Realmente es una cuestión de cuán confiablemente creas que se puede hacer la optimización, que se resuelve mejor perfilando y analizando las capacidades de los compiladores que tienes y posiblemente incluso desensamblando si hay un punto de acceso que no puedes descubrir dónde parece el compilador no han podido hacer una optimización obvia.

RVO es algo que ha existido durante siglos y, al menos excluyendo casos muy complejos, es algo que los compiladores han estado aplicando de manera confiable durante años. Definitivamente no vale la pena solucionar un problema que no existe.

Errar al lado de confiar en el optimizador, no temerlo

Por el contrario, diría que errar del lado de confiar demasiado en las optimizaciones del compilador que muy poco, y esta sugerencia proviene de un tipo que trabaja en campos muy críticos para el rendimiento, donde la eficiencia, el mantenimiento y la calidad percibida entre los clientes es todo un desenfoque gigante. Prefiero que confíes con demasiada confianza en tu optimizador y encuentres algunos casos oscuros en los que confías demasiado que confías demasiado poco y simplemente te encargas de temores supersticiosos todo el tiempo por el resto de tu vida. Al menos, eso hará que busques un perfilador e investiges adecuadamente si las cosas no se ejecutan tan rápido como deberían y obtendrás un valioso conocimiento, no supersticiones, en el camino.

Estás haciendo bien en apoyarte en el optimizador. Seguid así. No se convierta en ese tipo que comienza a solicitar explícitamente que se incorporen todas las funciones llamadas en un bucle antes de perfilarse por miedo equivocado a las deficiencias del optimizador.

Perfilado

La elaboración de perfiles es realmente la respuesta indirecta pero definitiva a su pregunta. El problema con el que los principiantes ansiosos por escribir código eficiente a menudo luchan no es qué optimizar, sino qué no optimizar porque desarrollan todo tipo de corazonadas equivocadas sobre ineficiencias que, aunque son intuitivamente humanas, son computacionalmente erróneas. El desarrollo de la experiencia con un generador de perfiles comenzará realmente a darle una apreciación adecuada no solo de las capacidades de optimización de sus compiladores en las que puede confiar con confianza, sino también de las capacidades (así como las limitaciones) de su hardware. Podría decirse que es aún más valioso perfilar para aprender lo que no valió la pena optimizar que aprender lo que fue.


fuente
-1

El software se puede escribir en C ++ en plataformas muy diferentes y para muchos propósitos diferentes.

Depende completamente del propósito del software. Si fuera fácil de mantener, expandir, parchear, refactorizar, etc. o son otras cosas más importantes, como el rendimiento, el costo o la compatibilidad con algún hardware específico o el tiempo que lleva desarrollarlo.

mathreadler
fuente
-2

Creo que la respuesta aburrida a esto es: "depende".

¿Es una mala práctica escribir código que se base en una optimización del compilador que probablemente esté desactivada y donde la vulnerabilidad no esté documentada y donde el código en cuestión no se pruebe unitariamente, de modo que si se rompe lo sabría ? Probablemente.

¿Es una mala práctica escribir código que se base en una optimización del compilador que probablemente no esté desactivada , que esté documentada y se pruebe la unidad ? Tal vez no.

Dave Cousineau
fuente
-6

A menos que haya más cosas que no nos está diciendo, esta es una mala práctica, pero no por la razón que sugiere.

Posiblemente, a diferencia de otros lenguajes que haya utilizado antes, devolver el valor de un objeto en C ++ produce una copia del objeto. Si luego modifica el objeto, está modificando un objeto diferente . Es decir, si tengo Obj a; a.x=1;y Obj b = a;luego lo hago b.x += 2; b.f();, entonces a.xigual es 1, no 3.

Entonces, no, el uso de un objeto como valor en lugar de como referencia o puntero no proporciona la misma funcionalidad y podría terminar con errores en su software.

Quizás lo sepa y no afecte negativamente su caso de uso específico. Sin embargo, según la redacción de su pregunta, parece que es posible que no tenga conocimiento de la distinción; redacción como "crear un objeto en la función".

"crear un objeto en la función" suena como new Obj;donde "devolver el objeto por valor" suena comoObj a; return a;

Obj a;y Obj* a = new Obj;son cosas muy, muy diferentes; el primero puede provocar daños en la memoria si no se usa y comprende correctamente, y el segundo puede provocar pérdidas de memoria si no se usa y comprende correctamente.

Aaron
fuente
8
La optimización del valor de retorno (RVO) es una semántica bien definida donde el compilador construye un objeto devuelto un nivel más arriba en el marco de la pila, evitando específicamente copias innecesarias de objetos. Este es un comportamiento bien definido que se ha respaldado mucho antes de que fuera obligatorio en C ++ 17. Incluso hace 10-15 años, todos los compiladores principales soportaban esta característica y lo hacían de manera consistente.
@Snowman No estoy hablando de la administración de memoria física de bajo nivel, y no hablé sobre la hinchazón o la velocidad de la memoria. Como mostré específicamente en mi respuesta, estoy hablando de los datos lógicos. Lógicamente , proporcionar el valor de un objeto es crear una copia del mismo, independientemente de cómo se implemente el compilador o qué ensamblaje se use detrás de escena. Las cosas de bajo nivel detrás de escena son una cosa, y la estructura lógica y el comportamiento del lenguaje son otra; están relacionados, pero no son lo mismo, ambos deben entenderse.
Aaron
66
su respuesta dice que "devolver el valor de un objeto en C ++ produce una copia del objeto", lo cual es completamente falso en el contexto de RVO: el objeto se construye directamente en la ubicación de la llamada y nunca se realiza ninguna copia. Puede probar esto eliminando el constructor de copia y devolviendo el objeto que se construye en la returninstrucción que es un requisito para RVO. Además, luego pasa a hablar sobre palabras clave newy punteros, que no es de lo que se trata RVO. Creo que o no entiendes la pregunta, o RVO, o posiblemente ambos.
-7

Pieter B tiene toda la razón al recomendar el menor asombro.

Para responder a su pregunta específica, lo que esto (muy probablemente) significa en C ++ es que debe devolver std::unique_ptra al objeto construido.

La razón es que esto es más claro para un desarrollador de C ++ en cuanto a lo que está sucediendo.

Aunque lo más probable es que su enfoque funcione, está indicando efectivamente que el objeto es un tipo de valor pequeño cuando, de hecho, no lo es. Además de eso, está descartando cualquier posibilidad de abstracción de interfaz. Esto puede estar bien para sus propósitos actuales, pero a menudo es muy útil cuando se trata de matrices.

Aprecio que si vienes de otros idiomas, todos los sigilos pueden ser confusos inicialmente. Pero tenga cuidado de no asumir que, al no usarlos, aclara su código. En la práctica, lo contrario es probable que sea cierto.

Alex
fuente
Cuando fueres haz lo que vieres.
14
Esta no es una buena respuesta para los tipos que no realizan asignaciones dinámicas. Que el OP sienta que lo natural en su caso de uso es devolver por valor indica que sus objetos tienen una duración de almacenamiento automático en el lado de la persona que llama. Para objetos simples, no demasiado grandes, incluso una implementación ingenua de valor de copia-retorno será un orden de magnitud más rápido que una asignación dinámica. (Si, por otro lado, la función devuelve un contenedor, devolver un unique_pointer puede incluso ser ventajoso en comparación con un ingenuo retorno del compilador por valor.)
Peter A. Schneider
99
@Matt En caso de que no se haya dado cuenta, esta no es la mejor práctica. Hacer innecesariamente asignaciones de memoria y forzar la semántica del puntero en los usuarios es malo.
nwp
55
En primer lugar, cuando se usan punteros inteligentes, uno debe regresar std::make_unique, no std::unique_ptrdirectamente. En segundo lugar, RVO no es una optimización esotérica específica del proveedor: está integrada en el estándar. Incluso cuando no lo era, tenía un amplio apoyo y comportamiento esperado. No tiene sentido devolver a std::unique_ptrcuando no se necesita un puntero en primer lugar.
44
@Snowman: No hay "cuando no era". Aunque recientemente se ha convertido en obligatorio , todos los estándares de C ++ han reconocido [N] RVO e hicieron ajustes para habilitarlo (por ejemplo, al compilador siempre se le ha dado permiso explícito para omitir el uso del constructor de copia en el valor de retorno, incluso si tiene efectos secundarios visibles).
Jerry Coffin