c++ - referencia - tipos de funciones en c
Costo de los parĂ¡metros por defecto en C++ (3)
Me encontré con un ejemplo de " Efectivo C ++ en un entorno integrado " por Scott Meyers, donde se describieron dos formas de usar parámetros predeterminados: uno que fue descrito como costoso y el otro como una mejor opción.
Me estoy perdiendo la explicación de por qué la primera opción podría ser más costosa en comparación con la otra.
void doThat(const std::string& name = "Unnamed"); // Bad
const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better
En el primero, una std::string
temporal se inicializa desde el literal "Unnamed"
cada vez que se llama a la función sin un argumento.
En el segundo caso, el objeto defaultName
se inicializa una vez (por archivo de origen) y se usa simplemente en cada llamada.
Tal vez malinterprete "costoso" (para la interpretación "correcta" vea la otra respuesta), pero una cosa a considerar con los parámetros predeterminados es que no se escalan bien en situaciones como esa:
void foo(int x = 0);
void bar(int x = 0) { foo(x); }
Esto se convierte en una pesadilla propensa a errores una vez que agregue más anidamientos porque el valor predeterminado debe repetirse en varios lugares (es decir, costoso en el sentido de que un pequeño cambio requiere cambiar lugares diferentes en el código). La mejor manera de evitar eso es como en tu ejemplo:
const int foo_default = 0;
void foo(int x = foo_default);
void bar(int x = foo_default) { foo(x); } // no need to repeat the value here
void doThat(const std::string& name = "Unnamed"); // Bad
Esto es "malo" en el sentido de que se crea una nueva std::string
con el contenido "Unnamed"
cada vez que se llama a doThat()
.
Digo "mal" y no mal porque la pequeña optimización de cadena en cada compilador de C ++ que he usado colocará los datos "Unnamed"
dentro de la std::string
temporal temporal creada en el sitio de la llamada y no asignará ningún almacenamiento para ella. Entonces, en este caso específico , el argumento temporal tiene poco costo. El estándar no requiere la optimización de cadenas pequeñas, pero está diseñado explícitamente para permitirlo, y cada biblioteca estándar actualmente en uso lo implementa.
Una cadena más larga causaría una asignación; La optimización de cadenas pequeñas funciona solo en cadenas cortas. Las asignaciones son caras; Si usa la regla general de que una asignación es 1000 veces más costosa que una instrucción habitual (¡ varios microsegundos! ), no estará muy lejos.
const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better
Aquí creamos un nombre defaultName
global con el contenido "Unnamed"
. Esto se crea en el momento de la inicialización estática. Hay algunos riesgos aquí; Si se llama doThat
en el momento de la inicialización o destrucción estática (antes o después de las ejecuciones main
), podría invocarse con un nombre defaultName
no defaultName
o uno que ya haya sido destruido.
Por otro lado, no hay riesgo de que ocurra una asignación de memoria por llamada aquí.
Ahora, la solución correcta en el c ++ 17 moderno es:
void doThat(std::string_view name = "Unnamed"); // Best
que no se asignará incluso si la cadena es larga; ¡Ni siquiera copiará la cadena! Además de eso, en los casos 999/1000, se trata de un reemplazo doThat
la antigua API doThat
e incluso puede mejorar el rendimiento cuando pasa datos a doThat
y no confía en el argumento predeterminado.
En este punto, es posible que el soporte de c ++ 17 en el contenido incrustado no esté allí, pero en algunos casos podría ser en breve. Y la vista de cadenas es un aumento de rendimiento lo suficientemente grande como para que existan miles de tipos similares en la naturaleza que hacen lo mismo.
Pero la lección sigue siendo; No hagas operaciones caras en los argumentos por defecto. Y la asignación puede ser costosa en algunos contextos (especialmente en el mundo integrado).