El coste oculto de std::optional::value_or y cómo evitarlo con lazy eval
Una técnica sencilla para evitar cálculos innecesarios en C++ al usar optional, aprovechando la conversión de tipos.
En C++, el lema "no pagas por lo que no usas" a veces se queda corto. Un desarrollador ha explicado cómo std::optional::value_or puede ejecutar cálculos costosos aunque no se necesite el valor por defecto, y propone una solución minimalista con un struct Lazy.
El problema surge porque value_or es una función. En C++, los argumentos de las funciones se evalúan antes de llamarla. Por tanto, en a.value_or(complex_computation()), la función complex_computation() se ejecuta siempre, independientemente de si a tiene un valor o si está vacío. El resultado es que haces el trabajo dos veces si lo necesitas y una vez si no, pero nunca cero veces si pasas el argumento directamente.
La implementación interna de std::optional solo convierte el argumento al tipo objetivo en la rama de "caída" (cuando el optional está vacío). Esto significa que, si pudieras pasar el cálculo como algo que solo se ejecuta al convertirse, ahorrarías ese ciclo.
La solución del artículo es un struct auxiliar Lazy que guarda una función miembro. Este struct tiene un operador de conversión genérico (operator T()) que invoca la función guardada solo cuando el código intenta convertir el objeto al tipo esperado. Al pasar Lazy{complex_computation} a value_or, el cálculo queda envuelto. Si el optional tiene valor, la conversión no ocurre y el cálculo se salta. Si está vacío, la conversión dispara la función y devuelve el resultado.
Este enfoque tiene limitaciones claras. No hay memorización: si reusas la misma instancia de Lazy en varias líneas, el cálculo se repetirá cada vez que se necesite el valor por defecto. Además, el operador de conversión no tiene restricciones, lo que permite compilar código como if (Lazy{...}) que podría comportarse de forma inesperada.
La noticia es relevante para cualquiera que trabaje con C++ moderno y utilice std::optional para manejar valores opcionales. Aunque C++23 introdujo or_else para resolver este caso específico con una función aceptada, el patrón Lazy ofrece una solución genérica para otras funciones del STL que sufren del mismo problema de evaluación anticipada, como std::map::try_emplace. Es una lección sobre la evaluación de argumentos que vale la pena recordar al optimizar código en caliente.
El autor indica que en una próxima entrada mejorará este helper para añadir memorización y restricciones de tipo, midiendo el coste de rendimiento de la sobrecarga.


