<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://headerfiles.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://headerfiles.com/" rel="alternate" type="text/html" /><updated>2026-05-09T18:07:15+00:00</updated><id>https://headerfiles.com/feed.xml</id><title type="html">Header Files</title><subtitle>Blog personal de Carlos Buchart con artículos sobre C++, Qt, buenas prácticas de programación y expresividad, desarrollo en general y más.</subtitle><author><name>Carlos Buchart</name></author><entry><title type="html">Optimizando Mapas</title><link href="https://headerfiles.com/2026/05/09/maps-and-sets" rel="alternate" type="text/html" title="Optimizando Mapas" /><published>2026-05-09T15:00:00+00:00</published><updated>2026-05-09T15:00:00+00:00</updated><id>https://headerfiles.com/2026/05/09/optimizando-mapas</id><content type="html" xml:base="https://headerfiles.com/2026/05/09/maps-and-sets"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>Trabajar con contenedores clave y clave-valor es algo que hacemos en casi cualquier aplicación: cachés, registros de configuración, índices, asociaciones entre datos… La biblioteca estándar de C++ nos proporciona varias opciones, cada una con sus fortalezas y debilidades. La opción obvia es <code class="language-plaintext highlighter-rouge">std::set</code> (para clave) <code class="language-plaintext highlighter-rouge">std::map</code> (para clave-valor), pero hay casos donde sencillas optimizaciones en estos contenedores pueden reportar ganancias significativas en rendimiento.</p>

<p>En este artículo compararemos algunos contenedores estándar, y buscaremos los mejores casos de uso para cada uno, así como sugerencias simples que mejorarán el rendimiento significativamente. Simplificaremos el estudio centrándonos en los contenedores clave-varlos (<code class="language-plaintext highlighter-rouge">std::map</code> y similares), pero en general las sugerencias y comparaciones son las mismas para los contenedores de conjuntos.</p>

<h2 id="stdmap-lo-obvio"><code class="language-plaintext highlighter-rouge">std::map</code>: lo obvio</h2>

<p>Cuando necesitamos un contenedor clave-valor ordenado, <code class="language-plaintext highlighter-rouge">std::map</code> es la primera opción que nos viene a la mente. Su API es directa: <code class="language-plaintext highlighter-rouge">insert</code> (<code class="language-plaintext highlighter-rouge">emplace</code>), <code class="language-plaintext highlighter-rouge">find</code>, <code class="language-plaintext highlighter-rouge">contains</code>, <code class="language-plaintext highlighter-rouge">erase</code>, <code class="language-plaintext highlighter-rouge">clear</code>, <code class="language-plaintext highlighter-rouge">operator[]</code>… Lo que ocurre internamente es que mantiene un árbol binario de búsqueda (típicamente un árbol rojo-negro), lo que garantiza que las claves siempre estén ordenadas y que operaciones como inserción, búsqueda y eliminación tengan complejidad O(log n).</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int32_t</span><span class="o">&gt;</span> <span class="n">items</span><span class="p">;</span>
<span class="n">items</span><span class="p">[</span><span class="s">"key1"</span><span class="p">]</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
<span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">items</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"key1"</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">it</span> <span class="o">!=</span> <span class="n">items</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">it</span><span class="o">-&gt;</span><span class="n">second</span> <span class="o">&lt;&lt;</span> <span class="sc">'\n'</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Hay pequeños cambios que son sencillos de aplicar y que pueden darnos mejoras en nuestro código. Por ejemplo, usar <code class="language-plaintext highlighter-rouge">emplace</code> en lugar de <code class="language-plaintext highlighter-rouge">insert</code> o de <code class="language-plaintext highlighter-rouge">operator[]</code> (en este segundo caso debemos tener cuidado ya que <code class="language-plaintext highlighter-rouge">emplace</code> no reemplaza el valor si la clave ya existe).</p>

<p>El orden de las claves es útil cuando necesitamos iterar sobre ellas de forma ordenada, y la complejidad logarítmica es aceptable para la mayoría de casos. Pero hay un problema que no es evidente a primera vista.</p>

<h2 id="el-problema-de-la-caché">El problema de la caché</h2>

<p><code class="language-plaintext highlighter-rouge">std::map</code> organiza los datos en un árbol disperso por la memoria. Cuando iteramos o realizamos búsquedas, los saltos entre nodos generan fallos de caché frecuentes. Tras un fallo de caché, el procesador debe esperar (típicamente unos cientos de ciclos) para obtener la siguiente línea de caché desde memoria principal. En una búsqueda logarítmica sobre millones de elementos, estos fallos se acumulan.</p>

<p>Comparemos esto con un contenedor lineal en memoria: cada acceso tiende a cargar datos vecinos útiles. Es el fenómeno conocido como <em>locality of reference</em>.</p>

<h2 id="el-problema-de-los-destructores">El problema de los destructores</h2>

<p>Imaginemos ahora otro escenario común: tenemos una función que crea un <code class="language-plaintext highlighter-rouge">std::map</code>, lo llena, lo usa y luego lo destruye. Si esta función se invoca frecuentemente (miles de veces por segundo), ¡el destructor se convierte en un punto caliente! El destructor debe:</p>

<ol>
  <li>Liberar la memoria de cada nodo</li>
  <li>Llamar al destructor de cada clave</li>
  <li>Llamar al destructor de cada valor</li>
</ol>

<p>Todo esto ocurre de forma dispersa en memoria. En aplicaciones con requisitos de latencia baja o rendimiento crítico, esto es problemático.</p>

<h2 id="stdvectorstdpairk-v-la-opción-contrarreformista"><code class="language-plaintext highlighter-rouge">std::vector&lt;std::pair&lt;K, V&gt;&gt;</code>: la opción contrarreformista</h2>

<p>¿Y si simplemente usáramos un <code class="language-plaintext highlighter-rouge">vector</code> ordenado de pares? Podríamos insertar todos los elementos, luego ordenarlo una única vez (ver detalles al final del artículo) y a partir de ahí realizar las búsquedas usando <code class="language-plaintext highlighter-rouge">std::lower_bound</code>. Siempre que no necesitemos encontrar elementos mientras insertamos, esta estrategia tiene ventajas interesantes.</p>

<p>La complejidad de búsqueda sigue siendo O(log n), como en <code class="language-plaintext highlighter-rouge">std::map</code>, pero con el beneficio en la caché que proporciona un array contiguo. La latencia de fallos de caché es varios órdenes de magnitud menor. La inserción de elementos es prácticamente gratuita (es un <code class="language-plaintext highlighter-rouge">emplace_back</code>, o mejor aún si hemos podido reservar toda la memoria de antemano con <code class="language-plaintext highlighter-rouge">reserve</code>), y toda la memoria se libera de una sola vez.</p>

<p>Como contrapartida, si necesitamos búsquedas mientras insertamos, o si modificamos y reordenamos constantemente, los costes se disparan. Pero para el patrón de “llenar, buscar, destruir”, este enfoque es extraordinariamente eficiente.</p>

<p>Además, la destrucción de este contenedor es mucho más eficiente, ya que no hay que recorrer multitud de nodos dispersos en memoria, sino que se realiza una única liberación de memoria al final.</p>

<h2 id="stdmap-vs-stdvector-ordenado"><code class="language-plaintext highlighter-rouge">std::map</code> vs <code class="language-plaintext highlighter-rouge">std::vector</code> ordenado</h2>

<p>Resumiendo la comparación principal del artículo:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">std::map&lt;K, V&gt;</code> mantiene orden e inserciones/búsquedas dinámicas en O(log n), pero paga con peor localidad de memoria y más coste en destrucción.</li>
  <li><code class="language-plaintext highlighter-rouge">std::vector&lt;std::pair&lt;K, V&gt;&gt;</code> ordenado también permite búsquedas en O(log n) tras ordenar, con mucha mejor localidad y destrucción más barata.</li>
  <li>Si el patrón es “insertar todo -&gt; ordenar -&gt; consultar”, <code class="language-plaintext highlighter-rouge">vector</code> suele ganar claramente.</li>
  <li>Si necesitamos mantener el orden y consultar mientras vamos insertando, <code class="language-plaintext highlighter-rouge">std::map</code> sigue siendo la opción natural.</li>
</ol>

<h2 id="potencia-amplificada-vectorpair-con-pmr">Potencia amplificada: <code class="language-plaintext highlighter-rouge">vector&lt;pair&gt;</code> con PMR</h2>

<p>C++17 introdujo memory resources (PMR). La idea es simple: en lugar de permitir que cada <code class="language-plaintext highlighter-rouge">vector</code> asigne su propia memoria, podemos proporcionarle un <em>resource</em> que gestiona dónde y cómo se asigna dicha memoria.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">pmr</span><span class="o">::</span><span class="n">unsynchronized_pool_resource</span> <span class="n">pool</span><span class="p">;</span>
<span class="n">std</span><span class="o">::</span><span class="n">pmr</span><span class="o">::</span><span class="n">memory_resource</span><span class="o">*</span> <span class="n">resource</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">pool</span><span class="p">;</span>

<span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">pmr</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">pmr</span><span class="o">::</span><span class="n">pair</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int32_t</span><span class="o">&gt;&gt;</span> <span class="n">items</span><span class="p">(</span><span class="n">resource</span><span class="p">);</span>

  <span class="c1">// use the vector</span>
<span class="p">}</span>
</code></pre></div></div>

<p>El beneficio es significativo cuando el patrón es realmente frecuente. No sólo evitamos nuevas asignaciones (lo que libera presión en el allocator), sino que la latencia se regulariza porque la memoria ya está “caliente” en caché.</p>

<p><em>Nota:</em> Si se usa C++14 o anterior, o el compilador no soporta PMR, siempre podemos recurrir a Boost.PMR, que contiene una implementación muy parecida.</p>

<h2 id="coletilla-stdunordered_map-si-no-necesitas-orden">Coletilla: <code class="language-plaintext highlighter-rouge">std::unordered_map</code> si no necesitas orden</h2>

<p>Si el orden de las claves no es relevante, <code class="language-plaintext highlighter-rouge">std::unordered_map</code> es una opción normalmente mejor que <code class="language-plaintext highlighter-rouge">std::map</code> (aunque no siempre mejor que el vector ordenado). Usa una tabla hash internamente, lo que reduce la complejidad de búsqueda a O(1) en promedio (en el caso de usar cadenas de caracteres como clave, la complejidad suele ser O(m)).</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">unordered_map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int32_t</span><span class="o">&gt;</span> <span class="n">items</span><span class="p">;</span>
<span class="n">items</span><span class="p">.</span><span class="n">emplace</span><span class="p">(</span><span class="s">"key1"</span><span class="p">,</span> <span class="mi">42</span><span class="p">);</span>
<span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">items</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"key1"</span><span class="p">);</span>
</code></pre></div></div>

<p>Aun así, el rendimiento real depende de la función hash y de la distribución de las claves. Cuando se usan cadenas de caracteres como clave, normalmente se habla de que la función hash es O(m), donde m es la longitud de la clave, es porque calcular el hash obliga a procesar todos los caracteres de la clave. Además, igual que <code class="language-plaintext highlighter-rouge">std::map</code>, no elimina por sí mismo el coste de destruir claves y valores si el contenedor se crea y destruye continuamente, aunque el <em>overhead</em> de destruir el contenedor como tal es menor ya que no está disperso en memoria.</p>

<h2 id="benchmarks">Benchmarks</h2>

<p>Sin entrar en grandes comparativas, a continuación mostramos una comparativa entre los tres contenedores mencionados en este artículo. En este experimento creamos N elementos aleatorios (la clave puede repetirse), buscamos N valores aleatorios, y luego destruimos el contenedor. Usamos 3 casos (de arriba a abajo): N = 10K, N = 100K, N = 1M.</p>

<p><img src="/assets/images/map_unordered_sorted_vector_10K.png" alt="Comparando std::map, std::unordered_map y std::vector ordenado, 10K" /></p>

<p><img src="/assets/images/map_unordered_sorted_vector_100K.png" alt="Comparando std::map, std::unordered_map y std::vector ordenado, 100K" /></p>

<p><img src="/assets/images/map_unordered_sorted_vector_1M.png" alt="Comparando std::map, std::unordered_map y std::vector ordenado, 1M" /></p>

<p>Podemos como el <code class="language-plaintext highlighter-rouge">std::vector</code> ordenado es más eficiente que los dos contenedores estándar cuando el número de elementos aumenta, lo que nos muestra la importancia de considerar la caché en nuestras optimizaciones.</p>

<h2 id="bonus-lo-mismo-aplica-a-stdset">Bonus: lo mismo aplica a <code class="language-plaintext highlighter-rouge">std::set</code></h2>

<p>Como mencionamos al principio, todo lo anterior es directamente aplicable a <code class="language-plaintext highlighter-rouge">std::set</code> y <code class="language-plaintext highlighter-rouge">std::unordered_set</code>. El razonamiento, las optimizaciones y los trade-offs son idénticos; simplemente sin el componente “valor”.</p>

<h2 id="conclusiones">Conclusiones</h2>

<p>La elección del contenedor clave-valor adecuado requiere entender más allá de la complejidad asintótica. Los detalles de memoria, caché y ciclo de vida del objeto son relevantes. En muchos casos, una solución “ingenua” resulta más eficiente que la opción obvia porque juega mejor con las características del hardware moderno.</p>

<p>La recomendación genérica sigue siendo: medir antes de optimizar. Pero si identificamos que un contenedor clave-valor es cuello de botella, empezaremos por considerar si afecta en realidad al caché, y si el contenedor tiene vida corta; si es así, <code class="language-plaintext highlighter-rouge">vector&lt;pair&gt;</code> ordenado probablemente sorprenderá.</p>

<h2 id="apéndice-ordenando-stdvector">Apéndice: ordenando <code class="language-plaintext highlighter-rouge">std::vector</code></h2>

<p>Al ordenar el <code class="language-plaintext highlighter-rouge">std::vector</code> debemos tener en cuenta la forma en que se comporte la inserción de elementos en un <code class="language-plaintext highlighter-rouge">std::map</code>: si se inserta un elemento con una clave ya existente, el valor se reemplaza. Para expresar esto en nuestro patrón <em>insertar luego ordenar</em> debemos primero mantener el orden de inserción durante el ordenamiento, y luego quedarnos con el último elemento del grupo que use la misma clave.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">pair</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">int32_t</span><span class="o">&gt;&gt;</span> <span class="n">items</span><span class="p">;</span>

<span class="c1">// (Insert data here)</span>

<span class="c1">// Sort, keeping insertion order</span>
<span class="n">std</span><span class="o">::</span><span class="n">stable_sort</span><span class="p">(</span><span class="n">items</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">items</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="p">[](</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">a</span><span class="p">.</span><span class="n">first</span> <span class="o">&lt;</span> <span class="n">b</span><span class="p">.</span><span class="n">first</span><span class="p">;</span>
<span class="p">});</span>

<span class="c1">// Remove duplicates and keep last one</span>
<span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">unique</span><span class="p">(</span><span class="n">items</span><span class="p">.</span><span class="n">rbegin</span><span class="p">(),</span> <span class="n">items</span><span class="p">.</span><span class="n">rend</span><span class="p">(),</span> <span class="p">[](</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">a</span><span class="p">.</span><span class="n">first</span> <span class="o">==</span> <span class="n">b</span><span class="p">.</span><span class="n">first</span><span class="p">;</span>
<span class="p">});</span>
<span class="n">items</span><span class="p">.</span><span class="n">erase</span><span class="p">(</span><span class="n">items</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">it</span><span class="p">.</span><span class="n">base</span><span class="p">());</span>

<span class="c1">// Binary searches</span>
<span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">lower_bound</span><span class="p">(</span><span class="n">items</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">items</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="s">"key1"</span><span class="p">,</span>
  <span class="p">[](</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">a</span><span class="p">.</span><span class="n">first</span> <span class="o">&lt;</span> <span class="n">b</span><span class="p">.</span><span class="n">first</span><span class="p">;</span> <span class="p">});</span>
</code></pre></div></div>]]></content><author><name>Carlos Buchart</name></author><category term="c++" /><category term="maps" /><category term="sets" /><category term="optimization" /><category term="performance" /><summary type="html"><![CDATA[Estudiamos los contenedores clave y clave-valor, y algunas optimizaciones sencillas]]></summary></entry><entry><title type="html">Mejorando los tiempos de compilación</title><link href="https://headerfiles.com/2025/08/28/mejorar-tiempos-compilacion" rel="alternate" type="text/html" title="Mejorando los tiempos de compilación" /><published>2025-08-28T09:00:00+00:00</published><updated>2025-08-28T09:00:00+00:00</updated><id>https://headerfiles.com/2025/08/28/mejorar-tiempos-compilacion</id><content type="html" xml:base="https://headerfiles.com/2025/08/28/mejorar-tiempos-compilacion"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>Como todos los lectores sabrán, C++ es un lenguaje compilado, lo que implica que su código fuente se transforma en un archivo binario ejecutable. No es necesario que el usuario final realice este proceso, lo hace el desarrollador. Generalmente es un proceso bastante rápido, pero a medida que el proyecto comienza a crecer también lo hace el tiempo requerido para su compilación. No es extraño encontrar proyectos de mediano tamaño que tardan 15-20 minutos en compilarse, y personalmente he trabajado con algunos que requerían unas 13 horas partiendo de cero.</p>

<p>Una de las primeras formas de reducir el tiempo de compilación usadas fue la de evitar compilar cosas que no fuesen necesarias: si un fichero ni ninguna de sus dependencias ni parámetros han sido modificados, una nueva compilación devolverá el mismo fichero objeto, por lo que una comparación de los <em>timestamps</em> entre ficheros fuente y objeto ayudará a centrarnos en aquellos que sí requieren ser procesados nuevamente. Este mecanismo es estándar en cualquier sistema de compilación, bien sean Makefiles, proyectos de MSVC, Xcode, etc.</p>

<p>Aún así, esto no suele ser suficiente, siendo frecuente tener que esperar entre 30 segundos y 5 minutos hasta que la compilación termine. Si sumamos el hecho de que tenemos que compilar multitud de veces cada día, en total supone un montón de tiempo dedicado a ver pasar líneas frente a nuestros ojos. Un día normal para mí conlleva entre 30 y 50 compilaciones, con una duración media de 30s, lo que nos da entre 15 y 25 minutos por jornada.</p>

<p><img src="/assets/images/compiling.png" alt="Créditos: https://xkcd.com/303/" /></p>

<p>Casi todos aprovechamos estos micro-descansos durante las compilaciones para hacer <em>algo en paralelo</em>. Dos minutos apenas da tiempo para ir al baño, en 4 podemos hacernos un café (contando el tiempo de ir a la cocina, calentar la leche, etc.). Podemos aprovechar para responder un email o mirar el estado del equipo, etc. Pero, salvo contadas ocasiones, la experiencia dicta que es mejor <em>no hacer nada</em>, no sólo porque lo que creíamos que era cosa de 1 minuto se extienda, sino porque además hacer muchas cosas al mismo tiempo significa perder el enfoque: el <em>cambio de contexto</em> penaliza la tarea principal. Lo mejor que podemos hacer es tratar de minimizar ese tiempo muerto (obviamente estoy asumiendo que compilamos cuando lo necesitamos y no como tic nervioso).</p>

<p>En este artículo comentaremos algunas estrategias de optimización (todas centradas en reducir la cantidad de código a compilar, ya veremos por qué), cómo aplicarlas eficientemente, y aspectos a tener en cuenta para que no nos salga el tiro por la culata.</p>

<h2 id="poniendo-a-dieta-al-compilador">Poniendo a dieta al compilador</h2>

<p>El proceso de compilación de proyectos en C++ se lleva a cabo, normalmente, en tres etapas: preprocesado, compilación y enlazado. Simplificando mucho, diremos que en la primera el fichero de código fuente es leído y convertido en un fichero de código fuente intermedio completamente autocontenido (es decir, que toda la información necesaria para compilarse está presente en dicho fichero). En la etapa de compilación, cada uno de estos ficheros autocontenidos es analizado y las sentencias C++ son transformadas en código binario intermedio, aunque aún no es ejecutable. Es en la tercera etapa donde todas las unidades de compilación son enlazadas entre sí junto a las dependencias externas, resultando en el fichero ejecutable.</p>

<p>Primero que nada, es importante mencionar que el preprocesado no hace ningún tipo de análisis de código; más bien podríamos verlo como una serie de operaciones de edición de texto. Por ejemplo, las macros son operaciones de sustitución de bloques de código (buscar/reemplazar), la inclusión de ficheros de cabecera es como un copiar/pegar, y la compilación condicional es como eliminación de código. Es durante la compilación donde se determina si el fichero tiene una sintaxis C++ válida y se identifica qué partes de todo el código compilado son realmente necesarias.</p>

<p>Como podemos intuir, la compilación más rápida será aquella que sólo compile lo que realmente estamos necesitando.</p>

<h2 id="análisis-de-la-compilación">Análisis de la compilación</h2>

<p>Es posible que muchos <em>managers</em> no entiendan el beneficio de reducir en 3 segundos el proceso de compilación. La forma más sencilla de justificarlo es, claramente, desde el punto de vista económico: 3 segundos por 30 veces diarias por 20 días al mes es media hora que gano al mes para el proyecto <em>por programador</em>. Lo mismo aplica para las <em>pipelines</em> de CI/CD.</p>

<p>De todas formas, si para conseguir esos 3 segundos hemos necesitado 3 días de trabajo, no hemos mejorado el proceso, ya que el ROI (<em>Return on Investment</em>) será muy bajo. Pero si por el contrario lo hemos hecho en una hora, habrá merecido la pena.</p>

<p>Se ve claramente que, como en toda optimización, lo primero que tenemos que hacer es averiguar dónde necesitamos optimizar, para atacar los problemas que nos den el mayor salto posible. Y es que casi siempre los cuellos de botella están centrados en unos pocos lugares.</p>

<p>Centraré la sección de <em>profiling</em> en clang, aunque el <em>modus operandi</em> en general es análogo en otros compiladores. Lo primero será activar la opción <code class="language-plaintext highlighter-rouge">-ftime-trace</code>, con la cual obtendremos un análisis de la compilación con el tiempo dedicado a cada etapa de la compilación y fichero (en MSVC sería <code class="language-plaintext highlighter-rouge">/timetrace</code>). El fichero generado se ubicará junto a los ficheros objeto intermedios, con el mismo nombre de la unidad de compilación pero extensión JSON. Podemos abrirlo con la herramienta de <em>tracing</em> incluida en cualquier navegador Chromium (por ejemplo, en Edge es <code class="language-plaintext highlighter-rouge">edge://tracing/</code>), o con otras como Perfetto (https://ui.perfetto.dev/).</p>

<h2 id="identificando-objetivos">Identificando objetivos</h2>

<p>Hay básicamente dos formas que uso para detectar los cuellos de botella. La primera sería analizar los ficheros JSON manualmente, y lo que suelo hacer en estos casos es ordenarlos por tamaño y centrarme en los más grandes; si bien no siguen un orden estricto, en general un fichero JSON grande significa que el <em>timetrace</em> recolectó muchos datos sobre el mismo. La segunda ya es más elaborada, y pasa por un <em>script</em> que lee los JSON y básicamente ensambla estadísticas globales del proyecto, incluyendo ficheros de mayor tiempo (medio), número de inclusiones y tiempo total.</p>

<p>Personalmente uso una combinación de ambas: con el <em>script</em> obtengo una clasificación (el podio), los ficheros que más impactan la compilación. Luego los analizo manualmente, abriendo cualquiera de los JSON referenciados para su análisis.</p>

<p>La siguiente imagen muestra las trazas para la compilación de un fichero .cpp modesto, de 2K líneas y 68KB de peso (es un ejemplo real, por lo que he ocultado algunos datos). Los resultados se muestran como un <em>flame graph</em>: el eje horizontal es el tiempo desde el inicio del procesado del fichero, donde cada bloque es una fase de la compilación y su ancho es el tiempo invertido en dicha fase; el eje vertical es el <em>call stack</em>.</p>

<p><img src="/assets/images/time-trace-overview.png" alt="Vista general de un timetrace" /></p>

<p>En este artículo nos centraremos en los bloques <em>verdes</em>, que representan la lectura, preprocesado y <em>parseo</em> (análisis sintáctico). Los bloques más grandes son nuestros principales cuellos de botella: ficheros que tardan mucho en ser analizados. Estos ficheros son los que tenemos que optimizar.</p>

<p>Ahora bien, debemos saber si, dado un bloque grande, el fichero asociado es pesado en sí mismo o porque otros (ficheros incluidos por el primero) lo <em>engordan</em>. Podemos obtener esta información en dos formas (complementarias entre sí): la primera es observar el eje vertical y el impacto de las dependencias (véase el bloque central: el primer fichero consume mucho, pero es obvio que es culpa de dos dependencias). La otra es observando los detalles de cada bloque (basta con seleccionar el bloque y ver el pánel inferior):</p>

<p><img src="/assets/images/time-trace-details.png" alt="Detalle de un timetrace" /></p>

<p>Acá podemos ver que si bien el procesado del fichero toma 325ms (<em>wall duration</em>), el fichero como tal solo toma 2ms (<em>self time</em>).</p>

<p>Esta combinación de acciones permite centrar los esfuerzos en los ficheros más importantes, si no podría pasar que el análisis individual sin ningún tipo de guía nos lleve a optimizar ficheros con un impacto muy bajo en la compilación final del proyecto.</p>

<h2 id="estrategias-a-seguir">Estrategias a seguir</h2>

<p>A continuación comentaremos algunas de las acciones que podemos llevar a cabo para mejorar los tiempos de compilación, aunque podrían resumirse en quitar lo que no hace falta. El orden de exposición corresponde al que uso personalmente, siguiendo como criterio el tiempo que lleva aplicarlas.</p>

<ul>
  <li><strong>Incluir únicamente los ficheros de cabecera necesarios.</strong> Es normal que, a lo largo de la historia del proyecto, el código haya cambiado mucho, por lo que es posible que haya ficheros de cabecera innecesarios, o que estemos incluyendo un <em>súper fichero</em> que contiene muchas cosas cuando sólo necesitamos una. Cualquier limpieza viene bien, y más aún si es de alguno de nuestros objetivos. Esta estrategia puede no salir siempre bien en el caso de que otro fichero de cabecera del mismo documento esté incluyendo al que hemos borrado, con lo que no estamos ganando nada. Esta estrategia paga mejor cuando se aplica a limpiar los propios ficheros de cabecera de <em>includes</em> innecesarios, ya que el impacto se multiplica. Truco: algunos IDEs y analizadores estáticos proveen esta información de forma directa.</li>
  <li><strong>Utilizar <em>forward-declarations</em>.</strong> Si nuestro fichero de cabecera sólo hace uso de una clase como una referencia o puntero (es decir, no necesita saber detalles), podríamos eliminar el fichero de cabecera que la declara y sustituirla por un <em>forward declaration</em>.</li>
  <li><strong>Separar ficheros con múltiples declaraciones en ficheros individuales.</strong> Existe una regla de oro (aunque algo flexible) que dice que cada declaración debe ir en su propio fichero. Esto permite que incluyamos únicamente lo que necesitamos.</li>
  <li><strong>Extraer declaraciones anidadas.</strong> Es una especie de corolario de la estrategia anterior: si una clase define una clase o enumeración dentro de la misma, cualquier referencia a los segundos obligará a incluir a la primera por completo.</li>
  <li><strong>Utilizar el patrón <em>Pimpl</em>.</strong> No entraré en detalles, pero este patrón permite separar mejor la declaración de la implementación. Así no sólo evitamos incluir ficheros de cabecera que sólo son necesarios en la implementación, sino que además cualquier modificación en la misma no impacta a los ficheros que usan esta clase.</li>
  <li><strong>Refactorizar las clases grandes en clases más pequeñas y con menos responsabilidades.</strong> De nuevo, una forma de incluir sólo lo que se necesita, pero que además mejora enormemente el diseño reduciendo el acoplamiento entre módulos.</li>
</ul>

<h2 id="pasándonos-de-la-raya">Pasándonos de la raya</h2>

<p>Consideremos ahora algunas posibles desventajas de las estrategias mencionadas:</p>

<ul>
  <li>Un <em>forward-declaration</em> es un segundo lugar al que tenemos que prestar atención si hacemos algún cambio a un tipo de dato (especialmente su nombre) aunque normalmente esto implica únicamente un fallo de compilación.</li>
  <li>Tener decenas o cientos de mini-ficheros para definir <em>enums</em> puede ser tedioso de mantener; en algunas ocasiones bastará con tener unos pocos ficheros de “tipos” básicos, agrupados por componente o función.</li>
  <li>El uso del patrón <em>Pimpl</em> implica en la práctica una desreferencia de memoria adicional. En los sistemas modernos esto no suele ser un problema, pero convendría tenerlo presente si lo usamos en secciones de código donde el rendimiento es crítico (y el <em>profiler</em> ya nos ha dicho que el <em>pimpl</em> es el problema; no optimicemos prematuramente, primero el diseño).</li>
</ul>

<h2 id="conclusión">Conclusión</h2>

<p>Optimizar los tiempos de compilación no es sólo una cuestión de comodidad para el desarrollador, sino una inversión que mejora la productividad del equipo y reduce los costes del proyecto. Las estrategias presentadas en esta primera parte se centran en el principio fundamental de “compilar únicamente lo necesario”, atacando el problema en su origen: la cantidad de código que debe procesar el compilador.</p>

<p>La clave del éxito radica en medir antes de optimizar. Herramientas como <code class="language-plaintext highlighter-rouge">-ftime-trace</code> nos permiten identificar los verdaderos cuellos de botella y centrar nuestros esfuerzos donde realmente importa. No todos los ficheros tienen el mismo impacto, y una optimización bien dirigida puede resultar en mejoras significativas con un esfuerzo mínimo.</p>

<p>Recordemos que estas optimizaciones deben aplicarse con criterio: aunque las estrategias mencionadas son generalmente beneficiosas, siempre conviene evaluar el coste de mantenimiento frente a la ganancia obtenida.</p>

<p>El tiempo de compilación perdido nunca se recupera, pero el tiempo invertido en optimizarlo se amortiza cada día.</p>]]></content><author><name>Carlos Buchart</name></author><category term="c++" /><category term="build" /><category term="compilation" /><summary type="html"><![CDATA[Mencionamos algunas estrategias para la reducción de tiempos de compilación en grandes proyectos.]]></summary></entry><entry><title type="html">5 cosas que puedes hacer al migrar a C++ moderno</title><link href="https://headerfiles.com/2025/08/14/5-cosas-que-puedes-hacer-al-migrar-a-cpp-moderno" rel="alternate" type="text/html" title="5 cosas que puedes hacer al migrar a C++ moderno" /><published>2025-08-14T06:00:00+00:00</published><updated>2025-08-14T06:00:00+00:00</updated><id>https://headerfiles.com/2025/08/14/5-cosas-que-puedes-hacer-al-migrar-a-cpp-moderno</id><content type="html" xml:base="https://headerfiles.com/2025/08/14/5-cosas-que-puedes-hacer-al-migrar-a-cpp-moderno"><![CDATA[<p>Si bien C++11 y el resto de versiones del bien llamado <em>C++ moderno</em> llevan ya tiempo entre nosotros, muchos programadores siguen usando C++ a la <em>antigua usanza</em>, en detrimento de la legibilidad, flexibilidad de diseño e incluso del rendimiento que nos ofrecen las versiones más recientes. He aquí algunas cosas que puedes hacer fácilmente para comenzar a disfrutar de las ventajas del <em>C++ moderno</em>.</p>

<h2 id="no-hagas-nada">No hagas nada</h2>

<p>Sí, como suena, la primera de ellas es “no hagas nada”. Haz un <em>benchmarking</em> de tu código antes y después de migrar y seguramente te sorprenderás, especialmente si haces un uso intensivo de contenedores de la biblioteca estándar.</p>

<p>Una de las principales razones de esto es la introducción de la semántica de movimiento, con la consecuencia añadida de que muchos de los métodos y constructores ya existentes han recibido soporte para argumentos <em>r-value</em>, lo que significa que, sin <em>mover</em> un dedo, disfrutamos ya de sus beneficios. Además, existen otras optimizaciones que pueden destacar dependiendo del compilador que usemos, tales como el <em>return-value-optimization</em> (RVO, obligatorio a partir de C++17).</p>

<p>El siguiente ejemplo muestra un caso no poco común de creación de un vector de objetos (adjunto también el código del <em>benchmark</em>):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">create_vector</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">n</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">s</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">v</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">v</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">s</span> <span class="o">+</span> <span class="n">s</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">v</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="n">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[])</span>
<span class="p">{</span>
    <span class="k">const</span> <span class="kt">size_t</span> <span class="n">n</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
    <span class="k">const</span> <span class="kt">size_t</span> <span class="n">m</span> <span class="o">=</span> <span class="n">atoi</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span>

    <span class="kt">int</span> <span class="n">z</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">char</span> <span class="n">c</span> <span class="o">=</span> <span class="p">(</span><span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="mi">26</span><span class="p">)</span> <span class="o">+</span> <span class="sc">'a'</span><span class="p">;</span>
        <span class="kt">size_t</span> <span class="n">sn</span> <span class="o">=</span> <span class="p">(</span><span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">+</span> <span class="mi">10</span><span class="p">;</span>
        <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">v</span> <span class="o">=</span> <span class="n">create_vector</span><span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="n">i</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">(</span><span class="n">sn</span><span class="p">,</span> <span class="n">c</span><span class="p">));</span>

        <span class="k">const</span> <span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="n">v</span><span class="p">.</span><span class="n">front</span><span class="p">().</span><span class="n">front</span><span class="p">();</span>
        <span class="n">z</span> <span class="o">+=</span> <span class="n">x</span><span class="p">;</span> <span class="c1">// to prevent compiler to remove code</span>
    <span class="p">}</span>

    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">z</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>El compilador usado ha sido Apple clang version 14.0.3 (clang-1403.0.22.14.1), todos con nivel de optimización <code class="language-plaintext highlighter-rouge">-O2</code> y bajo plataforma ARM. Todas las pruebas se han hecho con <code class="language-plaintext highlighter-rouge">n=10000</code> y <code class="language-plaintext highlighter-rouge">m=1000</code>, midiendo el tiempo de ejecución con <code class="language-plaintext highlighter-rouge">time</code> 5 veces y promediando los resultados:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--std=c++03</code>: real 5.586s, user 3.325s, system 2.010s</li>
  <li><code class="language-plaintext highlighter-rouge">--std=c++11</code>: real 1.856s, user 1.166s, system 0.689s</li>
  <li><code class="language-plaintext highlighter-rouge">--std=c++14</code>: real 1.875s, user 1.155s, system 0.692s</li>
  <li><code class="language-plaintext highlighter-rouge">--std=c++17</code>: real 1.829s, user 1.139s, system 0.689s</li>
  <li><code class="language-plaintext highlighter-rouge">--std=c++20</code>: real 1.839s, user 1.159s, system 0.674s</li>
</ul>

<p>Sin hacer nada hemos podido triplicar la velocidad de nuestro programa simplemente compilando con una versión más moderna del lenguaje. Esto obviamente no quiere decir que <em>todo</em> nuestro programa se acelere 3x; este ejemplo está preparado específicamente para mostrar esta mejora, pero da una idea clara de los beneficios que implican las nuevas características del lenguaje.</p>

<h2 id="auto-matiza-la-deducción-de-tipos">Auto-matiza la deducción de tipos</h2>

<p>C++11 introdujo un nuevo significado para la palabra reservada <code class="language-plaintext highlighter-rouge">auto</code> (es el único caso que conozco de cambios en este sentido). Se usa en una declaración para deducir el tipo de la variable a partir de su inicialización; y si el compilador no es capaz de hacer una deducción única, la declaración se considera incorrecta y se genera un error.</p>

<p>El uso de <code class="language-plaintext highlighter-rouge">auto</code> permite reducir la cantidad de código a escribir (y leer), simplificando el mismo, y moviendo el nivel de abstracción al <em>qué</em> en lugar del <em>cómo</em> (o <em>con qué</em>).</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">generate_ids</span><span class="p">(</span><span class="kt">int</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>

<span class="c1">// Before</span>
<span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">ids_old_way</span> <span class="o">=</span> <span class="n">generate_ids</span><span class="p">();</span>

<span class="c1">// Now</span>
<span class="k">const</span> <span class="k">auto</span> <span class="n">ids_old_way</span> <span class="o">=</span> <span class="n">generate_ids</span><span class="p">();</span>
</code></pre></div></div>

<p>Un caso especialmente útil es a la hora de usar iteradores:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">synonyms</span><span class="p">;</span>

<span class="c1">// Before</span>
<span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="o">&gt;::</span><span class="n">iterator</span> <span class="n">it</span> <span class="o">=</span> <span class="n">synonyms</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">word</span><span class="p">);</span>

<span class="c1">// Now</span>
<span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">synonyms</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">word</span><span class="p">);</span>
</code></pre></div></div>

<p>Además, se facilitan los <em>refactorings</em> y optimizaciones de código al reducir el número de errores de compilación: si se cambia el tipo de un contenedor, <code class="language-plaintext highlighter-rouge">auto</code> se deducirá el nuevo iterador y listo (obviamente, si el nuevo tipo de iterador no es compatible con el anterior sí puede haber problemas, por ejemplo si se pasa de un <code class="language-plaintext highlighter-rouge">std::vector</code> a un <code class="language-plaintext highlighter-rouge">std::unordered_map</code>). Lo mismo sucedería si se cambia el tipo de retorno de una función: con <code class="language-plaintext highlighter-rouge">auto</code> tendríamos ya hecha una parte del pastel.</p>

<p>La única <em>pega</em> (que por otra parte tiene su lado positivo), es que el nombre de la variable cobra más peso ya que no tenemos a la mano (¿ojo?) su tipo. Pero como dije, esto puede incluso mejorar el código al obligarnos a poner nombres descriptivos (<code class="language-plaintext highlighter-rouge">cardVector</code> podría ser <code class="language-plaintext highlighter-rouge">cardCollection</code> o simplemente <code class="language-plaintext highlighter-rouge">cards</code>, y esa enigmática <code class="language-plaintext highlighter-rouge">DatabaseConnectionController* dcc = DatabaseConnectionController::create()</code> pasar a ser <code class="language-plaintext highlighter-rouge">auto dbConnectionController = DatabaseConnectionController::create()</code>).</p>

<h2 id="pythoniza-tu-código">Pythoniza tu código</h2>

<p>El que diga que C++ moderno no <del>ha copiado</del> se ha inspirando en aspectos de otros lenguajes más jóvenes (especialmente Python), pues simplemente está negando lo obvio. Las nuevas sintaxis introducidas no sólo ayudan a hacer un código más compacto, sino que además permiten mejorar la expresividad del código y elevar el nivel de abstracción.</p>

<ul>
  <li>
    <p><em>Range-for</em>: seguramente la más conocida de estas <em>pythonizaciones</em>, permite recorrer una colección de elementos, sin necesidad de preocuparse del tipo exacto de contenedor. C++ ya disponía de un par de formas de hacerlo (un <code class="language-plaintext highlighter-rouge">for</code> desde <code class="language-plaintext highlighter-rouge">begin</code> hasta <code class="language-plaintext highlighter-rouge">end</code>, y el <code class="language-plaintext highlighter-rouge">std::for_each</code>), pero el <em>range-for</em> es más natural en muchos casos donde solamente queremos <em>recorrer los elementos</em> (pero no modificar el contenedor, por ejemplo).</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">container</span><span class="p">.</span><span class="n">size</span><span class="p">();</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// random-access iterators</span>
      <span class="n">foo</span><span class="p">(</span><span class="n">container</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
  <span class="p">}</span>

  <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">list</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;::</span><span class="n">iterator</span> <span class="n">it</span> <span class="o">=</span> <span class="n">list</span><span class="p">.</span><span class="n">begin</span><span class="p">();</span> <span class="n">it</span> <span class="o">!=</span> <span class="n">list</span><span class="p">.</span><span class="n">end</span><span class="p">();</span> <span class="o">++</span><span class="n">it</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// basic iterator version</span>
      <span class="n">foo</span><span class="p">(</span><span class="o">*</span><span class="n">it</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">for</span> <span class="p">(</span><span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">container</span><span class="p">.</span><span class="n">begin</span><span class="p">();</span> <span class="n">it</span> <span class="o">!=</span> <span class="n">container</span><span class="p">.</span><span class="n">end</span><span class="p">();</span> <span class="o">++</span><span class="n">it</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// more generic using 'auto'</span>
      <span class="n">foo</span><span class="p">(</span><span class="o">*</span><span class="n">it</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="n">std</span><span class="o">::</span><span class="n">for_each</span><span class="p">(</span><span class="n">container</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">container</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="n">foo</span><span class="p">);</span> <span class="c1">// using an algorithm</span>

  <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;&amp;</span> <span class="n">c</span> <span class="o">:</span> <span class="n">container</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// range-for</span>
      <span class="n">foo</span><span class="p">(</span><span class="n">c</span><span class="p">);</span>
  <span class="p">}</span>
</code></pre></div>    </div>

    <p>Como detalle curioso, he visto cómo el uso del <em>range-for</em> puede optimizar código en determinados momentos. Un <em>range-for</em> siempre copiará el iterador <code class="language-plaintext highlighter-rouge">end()</code>, por lo que si nuestro contenedor hacía uso de un <code class="language-plaintext highlighter-rouge">end()</code> costoso, eso que nos ahorramos.</p>

    <p>Por último, una rápida comparación entre <code class="language-plaintext highlighter-rouge">std::for_each</code> y los <em>range-for</em>:</p>

    <ul>
      <li>Los <em>range-for</em> permiten utilizar las instrucciones <em>break</em> y <em>continue</em> para modificar el flujo.</li>
      <li><code class="language-plaintext highlighter-rouge">std::for_each</code> puede ser paralelizado (C++17, ver más abajo).</li>
    </ul>
  </li>
  <li>
    <p>Utiliza las listas de inicialización, de esta forma puedes inicializar colecciones de datos en la propia declaración, e incluso <a href="/2023/03/27/que-conste-porque-construyo-con-constantes">hacerlas constantes</a>.</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">numbers</span> <span class="o">=</span> <span class="p">{</span>
      <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="s">"one"</span><span class="p">},</span>
      <span class="p">{</span><span class="mi">2</span><span class="p">,</span> <span class="s">"two"</span><span class="p">},</span>
      <span class="p">{</span><span class="mi">3</span><span class="p">,</span> <span class="s">"three"</span><span class="p">},</span>
  <span class="p">};</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Mejora la expresividad <em>atando</em> variables. Devolver pares o tuplas es una forma común de evitar crear <em>structs</em> específicamente para devolver varios valores en una función. Ahora bien, el problema surge rápidamente cuando no sabemos qué significan el <em>.first</em> o el <em>.second</em>, y peor aún si comparten el mismo tipo de datos.</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">std</span><span class="o">::</span><span class="n">pair</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">get_id_and_name</span><span class="p">();</span>

  <span class="c1">// Without binding</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">id_and_name</span> <span class="o">=</span> <span class="n">get_id_and_name</span><span class="p">();</span>
  <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"ID: "</span> <span class="o">&lt;&lt;</span> <span class="n">id_and_name</span><span class="p">.</span><span class="n">first</span> <span class="o">&lt;&lt;</span> <span class="s">", name: "</span> <span class="o">&lt;&lt;</span> <span class="n">id_and_name</span><span class="p">.</span><span class="n">second</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>

  <span class="c1">// With binding</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="p">[</span><span class="n">id</span><span class="p">,</span> <span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">get_id_and_name</span><span class="p">();</span>
  <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"ID: "</span> <span class="o">&lt;&lt;</span> <span class="n">id</span> <span class="o">&lt;&lt;</span> <span class="s">", name: "</span> <span class="o">&lt;&lt;</span> <span class="n">name</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
</code></pre></div>    </div>

    <p>También puede usarse al iterar sobre mapas:</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">roman_numbers</span><span class="p">;</span>

  <span class="c1">// Without binding</span>
  <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">it</span> <span class="o">:</span> <span class="n">roman_numbers</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Number "</span> <span class="o">&lt;&lt;</span> <span class="n">it</span><span class="p">.</span><span class="n">first</span> <span class="o">&lt;&lt;</span> <span class="s">" is "</span> <span class="o">&lt;&lt;</span> <span class="n">it</span><span class="p">.</span><span class="n">second</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// With binding</span>
  <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="p">[</span><span class="n">decimal</span><span class="p">,</span> <span class="n">roman</span><span class="p">]</span> <span class="o">:</span> <span class="n">roman_numbers</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Number "</span> <span class="o">&lt;&lt;</span> <span class="n">decimal</span> <span class="o">&lt;&lt;</span> <span class="s">" is "</span> <span class="o">&lt;&lt;</span> <span class="n">roman</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="haz-uso-de-los-nuevos-contenedores-y-métodos">Haz uso de los nuevos contenedores y métodos</h2>

<p>C++11 introduce nuevas estructuras de datos que mejoran drásticamente el rendimiento bajo determinadas condiciones:</p>

<ul>
  <li>Todo <code class="language-plaintext highlighter-rouge">std::map</code> cuyas claves sean tipos básicos (<code class="language-plaintext highlighter-rouge">char</code>, <code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">float</code>, enumeraciones, punteros, etc.), y con más de unas pocas decenas de elementos, puede ser reemplazado por <a href="https://es.cppreference.com/w/cpp/container/unordered_map"><code class="language-plaintext highlighter-rouge">std::unordered_map</code></a>. Es el equivalente de una tabla <em>hash</em> y sus operaciones son mucho más eficientes: O(1) de media para la inserción y la búsqueda, dependiendo de las colisiones que puedan generarse. También puede usarse con otros tipos, tales como <code class="language-plaintext highlighter-rouge">std::string</code> pero acá el rendimiento va a depender también del tamaño medio de la clave. Cuidado que con mapas de poco tamaño puede no notarse el rendimiento o incluso disminuir (el coste relativo de calcular la función <em>hash</em> respecto a la comparación del tipo bruto aumenta conforme el número de elementos es más pequeño).</li>
  <li>De forma análoga tenemos a <a href="https://es.cppreference.com/w/cpp/container/unordered_set"><code class="language-plaintext highlighter-rouge">std::unordered_set</code></a> como alternativa a <code class="language-plaintext highlighter-rouge">std::set</code>. En ambos casos es importante hacer notar que, tal y como indica su nombre, las claves no están ordenadas, por lo que hay que tener cuidado si la implementación actual depende de ello. Esto no debe de ser un impedimento por sí mismo para migrar; por ejemplo, si sólo se requieren las claves ordenadas para un proceso de serialización, y el rendimiento del mismo no es crítico, se podrían extraer las claves, ordenarlas y serializarlas en orden, manteniendo así la compatibilidad con el código anterior.</li>
  <li>Usar <a href="https://es.cppreference.com/w/cpp/string/basic_string_view"><code class="language-plaintext highlighter-rouge">std::string_view</code></a> (C++17) en los argumentos de funciones que no requieren modificar la cadena de texto. <code class="language-plaintext highlighter-rouge">std::string_view</code> es básicamente un <em>wrapper</em> al estilo de las cadenas de texto en C (un puntero al primer caracter y un tamaño) pero de forma segura y compatible con <code class="language-plaintext highlighter-rouge">std::string</code> donde haga falta. De esta forma, cuando se requiere un subconjunto de la cadena, se evita pasar copias innecesarias.</li>
</ul>

<p>Gracias a los <em>r-value</em> y a los <em>variadic templates</em>, C++11 introdujo nuevos métodos para añadir elementos a un contenedor de forma más eficiente. Tradicionalmente usamos <code class="language-plaintext highlighter-rouge">push_back</code> para añadir elementos a un <code class="language-plaintext highlighter-rouge">std::vector</code> o <code class="language-plaintext highlighter-rouge">insert</code> para los <code class="language-plaintext highlighter-rouge">std::map</code>. Ahora bien, en ambos casos el método primero reserva <em>e inicializa</em> el espacio para el elemento en el contenedor, y luego es que copia (o mueve) el elemento en sí. En la práctica esto significa que tenemos que llamar a un constructor por defecto y a un constructor de copia (o movimiento). En C++11 tenemos <code class="language-plaintext highlighter-rouge">std::vector::emplace_back</code> y <code class="language-plaintext highlighter-rouge">std::map::emplace</code> que nos permitirán construir <em>in-place</em> el elemento en su zona de memoria reservada, generando un código mucho más eficiente. Desgraciadamente, y por compatibilidad hacia atrás, los métodos anteriores <code class="language-plaintext highlighter-rouge">push_back</code> e <code class="language-plaintext highlighter-rouge">insert</code> no pudieron ganar esta mejora y la migración tenemos que hacerla a mano (además de cambiar costumbre de los métodos a usar).</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">my_bag</span> <span class="p">{</span>
    <span class="n">my_bag</span><span class="p">(</span><span class="kt">int32_t</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="n">b</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="n">c</span><span class="p">);</span>
<span class="p">};</span>

<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">my_bag</span><span class="o">&gt;</span> <span class="n">bags</span><span class="p">;</span>

<span class="c1">// Before</span>
<span class="n">bags</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">my_bag</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">));</span>

<span class="c1">// Now</span>
<span class="n">bags</span><span class="p">.</span><span class="n">emplace_back</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
</code></pre></div></div>

<p>Así, vemos que <code class="language-plaintext highlighter-rouge">emplace_back</code> se ha de llamar con los mismos argumentos del constructor. Si hiciese <code class="language-plaintext highlighter-rouge">my_bag bag{2, 3, 4}; bags.emplace_back(bag);</code> estaría llamando al constructor de copia, pero in-place, que sería una mejora más no la óptima.</p>

<h3 id="reduce-la-dependencia-de-bibliotecas-de-terceros">Reduce la dependencia de bibliotecas de terceros</h3>

<p>Como extensión del punto actual, y como ya se ha visto, C++11 y posteriores han ido extendiendo la biblioteca estándar con nuevos integrantes, muchas veces inspirándose en populares bibliotecas de terceros, especialmente Boost.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">std::thread</code>, <code class="language-plaintext highlighter-rouge">std::mutex</code>, para gestión de hilos y sincronización. <code class="language-plaintext highlighter-rouge">boost::thread</code> no es exactamente igual que <code class="language-plaintext highlighter-rouge">std::thread</code>, la de Boost tiene un conjunto mayor de funcionalidades, tales como interrupción de un hilo y manejo de colecciones de hilos.</li>
  <li><code class="language-plaintext highlighter-rouge">std::chrono</code>, para operaciones con unidades de tiempo.</li>
  <li><code class="language-plaintext highlighter-rouge">std::optional</code>, <code class="language-plaintext highlighter-rouge">std::variant</code>, para tipos opcionales y variantes tipo-seguras.</li>
  <li><code class="language-plaintext highlighter-rouge">std::filesystem</code> (C++17), para gestión del sistema de ficheros (aunque no es 100% equivalente).</li>
  <li><code class="language-plaintext highlighter-rouge">std::ranges</code> (C++20), inspirándose en ranges-v3.</li>
</ul>

<h2 id="mejora-la-gestión-de-recursos">Mejora la gestión de recursos</h2>

<p>Uno de los puntos que muchos desarrolladores critican a C++ es la gestión de memoria (punteros nulos, colgantes, etc). Es cierto que, tal y como comentaba Bjarne Stroustrup, <em>C hace que sea fácil pegarte un tiro en el pie; C++ lo hace más difícil, pero cuando lo haces te vuela toda la pierna</em>. Pero también es cierto que desde C++11 es aún más difícil ya que la biblioteca estándar provee de muchos mecanismos para evitarlo. Los dos principales son <code class="language-plaintext highlighter-rouge">std::unique_ptr</code> y <code class="language-plaintext highlighter-rouge">std::shared_ptr</code>. el primero permite expresar que un objeto tiene un único dueño, mientras el segundo distribuye, mediante un contador de referencias, la propiedad entre varios.</p>

<p>Un ejemplo común para <code class="language-plaintext highlighter-rouge">unique_ptr</code> son las clases <em>manager</em>, que centralizan el acceso a un determinado recurso. Así, esta clase puede tener un <code class="language-plaintext highlighter-rouge">unique_ptr</code> y pasar una referencia a todas las demás. Además, los <code class="language-plaintext highlighter-rouge">unique_ptr</code> no pueden ser copiados, sólo <em>movidos</em>, por lo que la transferencia de propiedad es explícita. Los <code class="language-plaintext highlighter-rouge">shared_ptr</code>, por otro lado, son más comunes en elementos con un ciclo de vida impredecible o donde los actores creadores del objeto pueden desaparecer antes que el objeto en sí.</p>

<p>Un código de C++ moderno no debería usar punteros <em>raw</em> para almacenar objetos. A la hora de pasar un objeto <code class="language-plaintext highlighter-rouge">unique_ptr</code> podemos o bien usar una referencia (que además obliga a no pasar un <code class="language-plaintext highlighter-rouge">nullptr</code>); si el objeto puede no estar inicializado podríamos pasar un <code class="language-plaintext highlighter-rouge">std::optional&lt;Objeto&amp;&gt;</code> (C++17), pero en este caso no hay una ventaja muy clara respecto a pasar un puntero <em>raw</em> ya que se puede usar mal en ambos casos. El acceso a punteros nulos sigue siendo responsabilidad del programador. Así que cuidado en este caso.</p>

<p>Ambos tipos de <em>punteros inteligentes</em> se basan en un principio muy conocido de C++ y del que ya he hablado en otras ocasiones: el RAII (ver <a href="/2020/01/13/automatizando-acciones-gracias-al-raii-parte-i/">RAII 1</a> y <a href="/2020/01/17/automatizando-acciones-gracias-al-raii-parte-ii/">RAII 2</a>). No me extenderé acá en este tema y refiero a dichas páginas para más información</p>

<h2 id="bonos">Bonos</h2>

<p>Algunos pequeños cambios adicionales que marcan una gran diferencia:</p>

<ul>
  <li>Nuevos literales: <code class="language-plaintext highlighter-rouge">""s</code> para <code class="language-plaintext highlighter-rouge">std::string</code>, <code class="language-plaintext highlighter-rouge">""ms</code> para <code class="language-plaintext highlighter-rouge">std::chrono::milliseconds</code>, <code class="language-plaintext highlighter-rouge">0x1234_u32</code> para enteros con tipo específico, etc. Hacen el código más expresivo y evitan conversiones implícitas.</li>
  <li>Paralelización de algoritmos (C++17): simplemente añade <code class="language-plaintext highlighter-rouge">std::execution::par</code> a tus <code class="language-plaintext highlighter-rouge">std::sort</code>, <code class="language-plaintext highlighter-rouge">std::transform</code> y otros algoritmos para aprovechar múltiples núcleos automáticamente.</li>
  <li><code class="language-plaintext highlighter-rouge">if constexpr</code> (C++17): cambia el complicado SFINAE por código estructurado más legible en templates. Permite escribir código condicional que se evalúa en tiempo de compilación.</li>
  <li>Designated initializers (C++20): inicializa estructuras de forma más clara con <code class="language-plaintext highlighter-rouge">Point{.x = 10, .y = 20}</code> en lugar de <code class="language-plaintext highlighter-rouge">Point{10, 20}</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">std::optional</code> (C++17): expresa explícitamente cuando una función puede no devolver un valor válido, eliminando la ambigüedad de los valores “especiales” como <code class="language-plaintext highlighter-rouge">-1</code> o <code class="language-plaintext highlighter-rouge">nullptr</code>.</li>
  <li>Fold expressions (C++17): simplifica operaciones en <em>parameter packs</em> con expresiones como <code class="language-plaintext highlighter-rouge">(args + ...)</code> en lugar de recursión manual.</li>
  <li>Lambda expressions mejoradas: desde C++11, pero con mejoras constantes. Usa <code class="language-plaintext highlighter-rouge">[&amp;]</code> para capturar por referencia, <code class="language-plaintext highlighter-rouge">[=]</code> por copia, o mezcla ambas. En C++14 puedes usar <em>generic lambdas</em> con <code class="language-plaintext highlighter-rouge">auto</code> en los parámetros.</li>
  <li><code class="language-plaintext highlighter-rouge">constexpr</code> everywhere: marca funciones como <code class="language-plaintext highlighter-rouge">constexpr</code> siempre que sea posible. El compilador las evaluará en tiempo de compilación cuando pueda, mejorando el rendimiento.</li>
  <li><code class="language-plaintext highlighter-rouge">std::array</code> vs arrays C: reemplaza <code class="language-plaintext highlighter-rouge">int arr[10]</code> por <code class="language-plaintext highlighter-rouge">std::array&lt;int, 10&gt;</code>. Obtienes los beneficios de los contenedores STL sin coste adicional.</li>
  <li>Inicialización uniforme: usa <code class="language-plaintext highlighter-rouge">{}</code> en lugar de <code class="language-plaintext highlighter-rouge">()</code> para la inicialización. Es más segura (previene <em>narrowing conversions</em>) y más consistente.</li>
</ul>

<h2 id="conclusión">Conclusión</h2>

<p>Migrar a C++ moderno no es solo cambiar el estándar del compilador; es adoptar una mentalidad que prioriza la expresividad, el rendimiento y la seguridad. Como hemos visto, algunas de estas mejoras llegan prácticamente “gratis” con solo recompilar el código, mientras que otras requieren cambios mínimos que pueden transformar drásticamente la calidad del software.</p>

<p>Los cinco puntos que hemos cubierto —aprovechar las optimizaciones automáticas, usar <code class="language-plaintext highlighter-rouge">auto</code> para simplificar el código, adoptar las sintaxis “pythonizadas”, migrar a contenedores más eficientes y mejorar la gestión de recursos— representan solo la punta del iceberg de lo que C++ moderno tiene para ofrecer.</p>

<p>La belleza del C++ moderno radica en que permite escribir código más limpio y expresivo sin sacrificar el rendimiento que siempre ha caracterizado al lenguaje. Al contrario, en muchos casos lo mejora. Así que la próxima vez que inicies un proyecto o tengas la oportunidad de refactorizar código existente, no dudes en darle una oportunidad a estas características. Tu código (y tus compañeros de equipo) te lo agradecerán.</p>]]></content><author><name>Carlos Buchart</name></author><category term="c++" /><category term="c++11" /><category term="c++14" /><category term="c++17" /><category term="c++20" /><summary type="html"><![CDATA[Descubre cómo migrar a C++ moderno puede triplicar el rendimiento de tu código y mejorar su legibilidad sin apenas esfuerzo. Desde optimizaciones automáticas hasta nuevas sintaxis y funciones.]]></summary></entry><entry><title type="html">La trampa al usar semántica de movimiento con std::string</title><link href="https://headerfiles.com/2024/07/17/std-string-move-trampa" rel="alternate" type="text/html" title="La trampa al usar semántica de movimiento con std::string" /><published>2024-07-16T22:00:00+00:00</published><updated>2024-07-16T22:00:00+00:00</updated><id>https://headerfiles.com/2024/07/17/std-string-move-trampa</id><content type="html" xml:base="https://headerfiles.com/2024/07/17/std-string-move-trampa"><![CDATA[<h2 id="la-trampa-al-usar-semántica-de-movimiento-con-stdstring">La trampa al usar semántica de movimiento con <code class="language-plaintext highlighter-rouge">std::string</code></h2>

<p>La semántica de movimiento de <code class="language-plaintext highlighter-rouge">std::string</code> puede ser complicada y, a menos que tengamos información previa sobre los tamaños esperados de las cadenas, puede tener el efecto contrario y hacer que el código sea más lento. La razón detrás de esto es la <em>optimización de cadenas pequeñas</em> (SSO, por sus siglas en inglés). que consiste, en resumidas cuentas, en tratar al objeto como si fuera una unión, de forma que si la cadena es más corta que un tamaño dado, se almacena en el mismo bloque de memoria del objeto en lugar de asignar memoria dinámica. Cuando la cadena supera ese tamaño, la cadena se almacena en un bloque diferente.</p>

<h3 id="qué-es-la-optimización-de-cadenas-pequeñas-sso">¿Qué es la Optimización de Cadenas Pequeñas (SSO)?</h3>

<p>La SSO es una técnica utilizada en la implementación de <code class="language-plaintext highlighter-rouge">std::string</code> para optimizar el uso de memoria y el rendimiento. En lugar de asignar memoria dinámica para todas las cadenas, la SSO almacena cadenas pequeñas directamente en el objeto <code class="language-plaintext highlighter-rouge">std::string</code> (como si de una unión se tratase). Se puede ver la SSO en acción en <a href="https://coliru.stacked-crooked.com/a/5ce54b634d60a59e">este ejemplo</a>.</p>

<p>Esta técnica evita la sobrecarga de la asignación de memoria dinámica, que puede ser costosa en términos de tiempo y recursos. Sin embargo, esta optimización introduce algunas consideraciones importantes al mover objetos <code class="language-plaintext highlighter-rouge">std::string</code>.</p>

<p><em>Nota: La SSO no es parte del estándar de C++ sino más bien una optimización de algunos compiladores. Igualmente, el tamaño máximo para considerar una cadena como pequeña no tiene que ser el mismo en todas las implementaciones ni plataformas.</em></p>

<h3 id="el-constructor-de-movimiento-de-stdstring">El constructor de movimiento de <code class="language-plaintext highlighter-rouge">std::string</code></h3>

<p>Al mover cualquier objeto en C++, estamos dando la posibilida de realizar una <em>copia optimizada</em>. La eficiencia aumenta cuando tenemos recursos externos que podemos intercambiar, como un puntero a un bloque de memoria o un <em>handle</em> de fichero. Sin embargo, para el resto de datos, aún tenemos que copiar datos. Si la cadena es pequeña y la SSO está en acción, no hay ningún puntero que intercambiar y todavía estamos copiando los datos base de <code class="language-plaintext highlighter-rouge">std::string</code>.</p>

<p>De hecho, al mover, tenemos que garantizar que el objeto original se mantenga en un estado válido, lo cual normalmente se hace estableciendo algunos valores por defecto. En la práctica, esto significa que estamos copiando una vez y asignando una vez, duplicando la cantidad de operaciones en comparación con una copia normal. Por lo tanto, si nuestras cadenas se espera que siempre (o la mayoría del tiempo) sean más cortas que el límite de SSO, entonces un movimiento perjudicaría el rendimiento.</p>

<h3 id="comparación-de-copia-vs-movimiento">Comparación de Copia vs Movimiento</h3>

<p>Para ilustrar mejor este punto, se puede comparar el rendimiento de la copia y el movimiento para cadenas pequeñas, grandes y una mezcla de ambas. El <a href="https://quick-bench.com/q/R8uD6hPu1z5yVSetQY5gblzX5R8">siguiente ejemplo</a> permite visualizar las diferencias entre ellas. En este benchmark, se estableció un tamaño de 32 caracteres para tener aproximadamente un 50% de cadenas pequeñas y un 50% de cadenas grandes. Los resultados muestran cómo el movimiento de cadenas pequeñas puede ser menos eficiente que una simple copia debido a la SSO.</p>

<p><img src="/assets/images/std-string-copy-move.png" alt="Benchmark std::string" /></p>

<h3 id="conclusión">Conclusión</h3>

<p>En resumen, la semántica de movimiento de <code class="language-plaintext highlighter-rouge">std::string</code> no siempre es la mejor opción, especialmente cuando se trata de cadenas cortas que se benefician de la SSO. Es crucial considerar el tamaño esperado de las cadenas al decidir entre copiar o mover <code class="language-plaintext highlighter-rouge">std::string</code>. Esta decisión puede tener un impacto significativo en el rendimiento de nuestra aplicación.</p>]]></content><author><name>Carlos Buchart</name></author><category term="c++" /><category term="c++11" /><category term="move-semantics" /><category term="string" /><summary type="html"><![CDATA[Estudiamos la eficiencia de la semántica de movimiento en std::string y exploramos algunas consideraciones.]]></summary></entry><entry><title type="html">Cómo llamar a una función una única vez</title><link href="https://headerfiles.com/2023/10/23/ejecutar-una-vez" rel="alternate" type="text/html" title="Cómo llamar a una función una única vez" /><published>2023-10-13T08:00:00+00:00</published><updated>2023-10-13T08:00:00+00:00</updated><id>https://headerfiles.com/2023/10/23/ejecutar-una-vez</id><content type="html" xml:base="https://headerfiles.com/2023/10/23/ejecutar-una-vez"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>Algunas veces es necesario tener funciones que han de llamarse una única vez en todo el ciclo de vida del proceso. El caso que más he visto es el de funciones de inicialización, tales como la configuración de un <em>framework</em> de terceros, la definición de variables de entorno o la creación de zonas de memoria compartidas.</p>

<p>Como pasa muchas veces, C++ nos ofrece no una, sino muchas formas de resolver el problema: estudiemos algunas de ellas (<em>spoiler</em>, dejaré mi favorita para el final). Para facilitar las explicaciones, asumiremos que el código a ejecutar está encapsulado en una función llamada <code class="language-plaintext highlighter-rouge">init_once()</code> que debe ser llamada antes de que <code class="language-plaintext highlighter-rouge">execute_many()</code> se ejecute.</p>

<h2 id="variable-bandera">Variable bandera</h2>

<p>Seguramente la solución más sencilla, aunque no necesariamente la más eficiente, es crear una variable a modo de bandera de uso (inicializada a <em>false</em>), y cambiarla la primera vez que se llame a la función.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span>
<span class="p">{</span>
    <span class="kt">bool</span> <span class="n">g_called</span><span class="p">{</span><span class="nb">false</span><span class="p">};</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">execute_many</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">g_called</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">init_once</span><span class="p">();</span>
        <span class="n">g_called</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="variante-con-variable-estática">Variante con variable estática</h3>

<p>Personalmente prefiero limitar el alcance de las variables todo lo posible, por lo que cambiaremos esta bandera a una variable estática local. Recordad que una variable estática se crea una única vez y perdura durante toda la vida del proceso.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">execute_many</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">static</span> <span class="kt">bool</span> <span class="n">s_called</span><span class="p">{</span><span class="nb">false</span><span class="p">};</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">s_called</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">init_once</span><span class="p">();</span>
        <span class="n">s_called</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Si bien presentan una solución simple, queda la sutil posibilidad de que cambiemos el valor de la bandera por error (por ejemplo, si tenemos varias funciones de inicialización). El tema de la eficiencia claramente dependerá del contexto, aunque la gran mayoría de las veces no será un problema. Por último, estas soluciones podrían originar una condición de carrera y desembocar en una doble inicialización.</p>

<h2 id="stdcall_once"><code class="language-plaintext highlighter-rouge">std::call_once</code></h2>

<p>C++11 introdujo una forma <em>estándar</em> de resolver este problema, y que además es <em>thread-safe</em>. Como ya se dijo, las dos soluciones anteriores pecarían de crear condiciones de carrera, necesitando el uso de <em>mutex</em> adicionales; el uso de <code class="language-plaintext highlighter-rouge">std::call_once</code> es equivalente pero mucho más limpio. Básicamente sigue el mismo modelo que la solución anterior: se asocia un <em>flag</em> especial (<em>thread-safe</em>) a la función que queremos llamar una única vez:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;mutex&gt;</span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">execute_many</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">static</span> <span class="n">std</span><span class="o">::</span><span class="n">once_flag</span> <span class="n">s_once</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">call_once</span><span class="p">(</span><span class="n">s_once</span><span class="p">,</span> <span class="n">init_once</span><span class="p">);</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="uso-de-singletons">Uso de <em>singletons</em></h2>

<p>Otra posible solución es emplear un <em>singleton</em>. Un <em>singleton</em> es un patrón de diseño que permite restringir la creación de objetos de una clase a una única instancia. Así, podemos utilizarlo para llamar a <code class="language-plaintext highlighter-rouge">init_once()</code> durante la construcción del mismo (y como la clase sólo se construye una vez, sólo se llamará a la función una única vez). Una ventaja de este método frente a los anteriores es que nos evitamos la comprobación de una bandera de estado para cada ejecución. Si la función <code class="language-plaintext highlighter-rouge">execute_many()</code> se llama de forma masiva, pues es una mejora que ganamos. En contrapartida, la función <code class="language-plaintext highlighter-rouge">execute_many</code> pasa a ser miembro del <em>singleton</em>.</p>

<p>Acá una implementación sencilla pero suficiente de un <em>singleton</em> con inicialización única:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Singleton</span>
<span class="p">{</span>
<span class="nl">public:</span>
    <span class="n">Singleton</span><span class="o">&amp;</span> <span class="n">get_instance</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">static</span> <span class="n">Singleton</span> <span class="n">s_singleton</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">s_singleton</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kt">void</span> <span class="n">execute_many</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

<span class="nl">private:</span>
    <span class="n">Singleton</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">init_once</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="kt">void</span> <span class="n">foo</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">Singleton</span><span class="o">::</span><span class="n">get_instance</span><span class="p">().</span><span class="n">execute_many</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="usando-el-operador-de-evaluación-secuencial-en-la-inicialización-de-una-variable-estática">Usando el operador de evaluación secuencial en la inicialización de una variable estática</h2>

<p>La última solución que expondré es, para mí, la más limpia en términos de código generado, aunque requiere un poco más de conocimiento del lenguaje para poder entenderla. Expliquemos primero las partes que lo componen:</p>

<h3 id="operador-de-evaluación-secuencial">Operador de evaluación secuencial</h3>

<p>El operador de evaluación secuencial es una expresión del tipo <em>(e<sub>0</sub>, e<sub>1</sub>, …, e<sub>n</sub>)</em>, donde las sub-expresiones <em>e<sub>i</sub></em> son evaluadas en orden y cuyo tipo y valor final corresponden a los de <em>e<sub>n</sub></em>. Así, la siguiente expresión <code class="language-plaintext highlighter-rouge">auto x = (42.0f, "hola"s)</code> resultaría en <code class="language-plaintext highlighter-rouge">x</code> de tipo <code class="language-plaintext highlighter-rouge">std::string</code> y con valor <code class="language-plaintext highlighter-rouge">"hola"</code>. Si una de las sub-expresiones fuese una llamada a función, ésta se invocaría, independientemente del tipo de retorno de la misma, incluido <code class="language-plaintext highlighter-rouge">void</code>. Por otra parte, si una de las sub-expresiones lanza una excepción, las siguientes sub-expresiones no serían evaluadas.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">a</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">a</span><span class="o">++</span><span class="p">,</span> <span class="o">++</span><span class="n">a</span><span class="p">,</span> <span class="n">a</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>

<span class="k">try</span> <span class="p">{</span>
    <span class="p">(</span><span class="n">a</span><span class="o">++</span><span class="p">,</span> <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">exception</span><span class="p">{},</span> <span class="n">a</span><span class="o">--</span><span class="p">);</span> <span class="c1">// a-- is never called</span>
<span class="p">}</span> <span class="k">catch</span><span class="p">(...)</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Exception"</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">a</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
</code></pre></div></div>

<p>El resultado es:</p>

<pre><code class="language-txt">2
Exception
3
</code></pre>

<p>Nótese que como son expresiones separadas, evaluadas secuencialmente, el uso del operador de post-incremento no se diferencia (en cuanto al resultado final) del de pre-incremento.</p>

<h3 id="inicialización-de-variable-estáticas">Inicialización de variable estáticas</h3>

<p>Por otro lado, las variables estáticas sólo se construyen una vez, y el estándar de C++ garantiza que la inicialización de una variable estática es <em>thread-safe</em>; es decir, si diversos hilos pasan concurrentemente por la inicialización de la variable, sólo uno de ellos, el primero, la efectuará, quedando los demás bloqueados hasta que finalice la inicialización.</p>

<h3 id="ensamblando-las-partes">Ensamblando las partes</h3>

<p>Con todo esto podemos construir una versión minimalista de nuestra solución, que garantizará que la función <code class="language-plaintext highlighter-rouge">init_once()</code> será llamada una única vez, de forma <em>thread-safe</em> y sin comprobaciones innecesarias de banderas de estado.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">execute_many</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">static</span> <span class="k">const</span> <span class="kt">bool</span> <span class="n">s_initialized</span> <span class="o">=</span> <span class="p">(</span><span class="n">init_once</span><span class="p">(),</span> <span class="nb">true</span><span class="p">);</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="extendiendo-la-solución">Extendiendo la solución</h2>

<p>El principio de responsabilidad única conlleva, por lo general, a descomponer nuestro código en clases y funciones con una finalidad más acotada. En el caso que nos ocupa hoy esto puede suponer aumentar el riesgo de que la función <code class="language-plaintext highlighter-rouge">init_once()</code> sea llamada desde diversos lugares, debiendo aplicar los mecanismos de protección expuestos más de una vez. Esto nos lleva al eterno dilema del programador: evitar duplicar código innecesariamente.</p>

<p>En términos generales, la solución pasa primero por limitar el acceso a la función en sí misma. Una primera forma de hacerlo es crear una clase cuya única razón de ser sea la de invocar a esta función:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">InitOnceCaller</span>
<span class="p">{</span>
<span class="nl">public:</span>
    <span class="k">static</span> <span class="kt">void</span> <span class="n">call_init_once</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">static</span> <span class="k">const</span> <span class="kt">bool</span> <span class="n">s_initialized</span> <span class="o">=</span> <span class="p">(</span><span class="n">init_once</span><span class="p">(),</span> <span class="nb">true</span><span class="p">);</span>
    <span class="p">}</span>

<span class="nl">private:</span>
    <span class="k">static</span> <span class="kt">void</span> <span class="n">init_once</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>La contrapartida acá es que debemos pagar por una llamada a función adicional en caso de que el compilador no la haga <em>inline</em>.</p>

<p>En caso de que la función deba ser llamada únicamente desde un punto en concreto, podríamos mover <code class="language-plaintext highlighter-rouge">init_once()</code> a una lambda local.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">execute_many</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">s_init_once</span> <span class="o">=</span> <span class="p">[]()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">};</span>
    <span class="k">static</span> <span class="k">const</span> <span class="kt">bool</span> <span class="n">s_initialized</span> <span class="o">=</span> <span class="p">(</span><span class="n">s_init_once</span><span class="p">(),</span> <span class="nb">true</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="conclusión">Conclusión</h2>

<p>Se han presentado varias formas de abordar el problema de inicialización única, yendo desde la más obvia y sencilla, hasta la más completa (aunque sutilmente críptica para los menos entendidos en el lenguaje), pasando por opciones intermedias en cuanto a legibilidad y rendimiento.</p>]]></content><author><name>Carlos Buchart</name></author><category term="c++" /><category term="initialization" /><summary type="html"><![CDATA[Estudiamos varias técnicas para restringir, de forma elegante, la ejecución de una función a una única vez.]]></summary></entry><entry><title type="html">Complejidad algorítmica (parte I)</title><link href="https://headerfiles.com/2023/07/17/complejidad-algoritmica-1" rel="alternate" type="text/html" title="Complejidad algorítmica (parte I)" /><published>2023-07-17T07:00:00+00:00</published><updated>2023-07-17T07:00:00+00:00</updated><id>https://headerfiles.com/2023/07/17/complejidad-algoritmica-1</id><content type="html" xml:base="https://headerfiles.com/2023/07/17/complejidad-algoritmica-1"><![CDATA[<h2 id="introducción">Introducción</h2>

<p>Sin entrar a filosofar demasiado, podríamos decir que para que un determinado código pueda considerarse bueno, hacen falta cinco cosas:</p>

<ul>
  <li>Hacer lo que tiene que hacer, es decir, cumplir con los requerimientos.</li>
  <li>No hacer lo que no debe hacer (no tener errores, ser seguro, ser fiable).</li>
  <li>Hacerlo eficientemente, con el menor consumo de recursos posible.</li>
  <li>Acoplarse correctamente al resto del sistema, sin interferir con otras aplicaciones.</li>
  <li>Ser entendible tanto por el equipo actual como por el del futuro (expresividad y documentación).</li>
</ul>

<p>Así como en otras ocasiones hemos hablado mucho del último punto, hoy (y en futuras entregas) lo haremos del tercero: eficiencia, y más específicamente de un aspecto del rendimiento llamado <em>complejidad algorítmica</em>. Aunque este tema ha sido abordado por numerosos autores de una forma mucha más profunda de lo que lo haremos acá, el objetivo de estas entradas es introducir el concepto y su importancia, así como dar ejemplos y guías rápidas de uso que nos permitan sacar provecho del mismo en nuestros proyectos.</p>

<h2 id="complejidad-algorítmica">Complejidad algorítmica</h2>

<p>El concepto de complejidad algorítmica se refiere a cómo se comporta un determinado código cuando el conjunto de datos sobre el que opera crece (se dice que su tamaño <em>tiende a infinito</em>). Es decir, nos habla principalmente de la <em>escalabilidad</em> del código, y también, aunque de forma indirecta, de su eficiencia.</p>

<p>La complejidad algorítmica suele evaluarse considerando dos aspectos: el temporal (tiempo de ejecución) y el espacial (memoria requerida). Aunque trataremos de abordar ambos a lo largo de estas entregas, nos centraremos en el análisis de tiempo, pudiéndose tomar la teoría y aplicarla directamente al espacial la gran mayoría de las veces.</p>

<p>Para realizar este análisis necesitaremos una forma de indicar la complejidad obtenida, y lo haremos utilizando la <em>notación asintótica</em>, más específicamente de la O grande (aunque existen otros tipos).</p>

<h2 id="notación-asintótica-o-grande">Notación asintótica O grande</h2>

<p>Esta notación indica una cota máxima en la complejidad de un algoritmo. Indica, <em>grosso modo</em>, cómo es el comportamiento de un algoritmo (en tiempo o espacio) a medida que crece el conjunto de datos. No se expresa en unidades de tiempo (o de memoria) específicas, ni siquiera en términos de instrucciones, ya que dependen de muchos factores (compilador, <em>flags</em> utilizados, arquitectura, hardware disponible, entorno, etc).</p>

<p>Tampoco es un análisis detallado del número de operaciones que un algoritmo realiza, o de los bytes que consume, sino un resumen de su tendencia principal. Así, un algoritmo que sume elemento a elemento dos vectores, y otro que realice 514 operaciones por cada par de elementos, tendrá la misma notación O grande (en este caso O(n), pero eso lo veremos en breve). ¿Por qué? Porque a medida que el conjunto de datos crece, los detalles de implementación tienen cada vez menos impacto frente al comportamiento general del mismo. (Obviamente, esto no quita que a la hora de comparar exhaustivamente dos algoritmos o implementaciones no debamos tomar en cuenta estos detalles, pero en esta serie nos centraremos en lo antes expuesto.)</p>

<p>La notación O grande busca pues describir, con sencillez, este comportamiento, de forma que podamos hacernos una idea del rendimiento de un algoritmo y poder realizar comparaciones entre distintas soluciones. Algunos de los tipos principales son (en orden de <em>mejor</em> a <em>peor</em>):</p>

<ul>
  <li>O(1): constante (el tiempo o espacio requerido no se ve afectado por el tamaño del conjunto de datos). Ejemplos son el acceso a un arreglo o vector de datos, consultas a tablas <em>hash</em>, y búsqueda de máximo o mínimo en un conjunto ordenado.</li>
  <li>O(log n): logarítmico (normalmente se descartan secciones completas del conjunto de datos durante el procesamiento). El tipo de algoritmo más conocido de este orden son las búsquedas dicotómicas (o binarias).</li>
  <li>O(n): lineal (seguramente el caso más trivial, recorrer los datos un número constante de veces). Se identifican rápidamente por la presencia de un bluce <em>for</em> del tipo <code class="language-plaintext highlighter-rouge">for (size_t i = 0; i &lt; N; ++i)</code> (o variantes).</li>
  <li>O(n log n): cuasi-lineal. La gran mayoría de algoritmos de ordenación eficientes (tales como <em>quick-sort</em>) tienen esta complejidad.</li>
  <li>O(n<sup>2</sup>): cuadrático (recorrer el conjunto de datos por cada elemento del mismo). Suelen consistir en un par de bucles anidados y, en muchos casos, corresponden a la versión más directa (y no optimizada) de un algoritmo.</li>
  <li>O(n<sup>3</sup>): cúbico. Análogamente al cuadrático, encontramos tres bucles anidados. Estos casos son raros de ver de forma directa y suelen aparecer disfrazados como la aplicación, a modo de subrutina, de un algoritmo cuadrático a cada elemento de un conjunto de datos.</li>
  <li>O(2<sup>n</sup>): exponencial. Un ejemplo son las búsquedas de caminos óptimos por fuerza bruta.</li>
</ul>

<h2 id="rendimiento-promedio-mejor-y-peor-caso">Rendimiento promedio, mejor y peor caso</h2>

<p>Lo más normal es medir el rendimiento de un algoritmo en los casos más comunes. Aún así, muchos algoritmos se comportan de forma más eficiente en determinadas situaciones. Por ejemplo, algunos algoritmos de ordenanamiento (entre ellos el <em>infame</em> algoritmo de la burbuja) pueden llegar a ser O(n) sobre conjuntos previamente ordenados. Así mismo, puede pasar que haya casos en los que el rendimiento decaiga dramáticamente (por poner otro ejemplo interesante, el <em>quick-sort</em> puede llegar a ser O(n<sup>2</sup>) si el conjunto está ordenado de forma inversa).</p>

<p>El conocimiento del comportamiento del algoritmo en todos estos casos nos proporcionará una guía útil para elegir el más acorde a nuestras necesidades.</p>

<h2 id="ejemplo">Ejemplo</h2>

<p>Para entenderlo mejor, veamos cómo se comportarían un grupo de funciones, todas calculando el mismo resultado pero cada una con una complejidad media diferente. Ya mencionamos anteriormente que la complejidad algorítmica no está asociada a tiempos específicos, pero ilustrar con algunos números reales siempre ayuda a entender mejor el concepto. Supongamos que para el caso básico (N=1) todas las variantes tardasen 0,1us (venga, un tiempo a primera vista <em>ridículamente pequeño</em>). Ahora, <em>midamos</em> (desde un punto de vista teórico y simplista) cuánto tardarían en ejecutarse estos algoritmos para N=100, N=10.000 y N=1.000.000:</p>

<table>
  <thead>
    <tr>
      <th>Complejidad</th>
      <th>1</th>
      <th>100</th>
      <th>10.000</th>
      <th>1.000.000</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>O(1)</td>
      <td>0,1us</td>
      <td>0,1us</td>
      <td>0,1us</td>
      <td>0,1us</td>
    </tr>
    <tr>
      <td>O(log n)</td>
      <td>0,1us</td>
      <td>0,6us</td>
      <td>1,3us</td>
      <td>2us</td>
    </tr>
    <tr>
      <td>O(n)</td>
      <td>0,1us</td>
      <td>10us</td>
      <td>1ms</td>
      <td>100ms</td>
    </tr>
    <tr>
      <td>O(n log n)</td>
      <td>0,1us</td>
      <td>66us</td>
      <td>13,3ms</td>
      <td>2s</td>
    </tr>
    <tr>
      <td>O(n<sup>2</sup>)</td>
      <td>0,1us</td>
      <td>1ms</td>
      <td>10s</td>
      <td>27,8h</td>
    </tr>
    <tr>
      <td>O(n<sup>3</sup>)</td>
      <td>0,1us</td>
      <td>100ms</td>
      <td>27,8h</td>
      <td>3.171y</td>
    </tr>
    <tr>
      <td>O(2<sup>n</sup>)</td>
      <td>0,1us</td>
      <td>🌌</td>
      <td>🤯</td>
      <td>🤯</td>
    </tr>
  </tbody>
</table>

<p>Nota: En este caso el algoritmo exponencial no nos serviría más que para conjunto de unas pocas unidades</p>

<p>Aunque pareciese que incluso los cuatro primeros tienen un rendimiento más que decente, tenemos que ponerlos en contexto. Para operaciones que se realizan una única vez, o muy esporádicamente, tiempos de hasta unos pocos segundos pueden ser aceptables (guardar un fichero, la generación de miniaturas de un álbum de fotos, preparar un documento para su impresión, precalcular tablas de valores). Por otro lado, si la operación debe ser realizada continuamente, o forma parte de un flujo de trabajo más largo, es probable que se convierta en nuestro cuello de botella y debamos buscar una alternativa.</p>

<p>Imaginemos que esta función es la encargada de calcular la colisión entre el personaje de un videojuego y su entorno, y donde N es la cantidad de polígonos en la escena. Si queremos un juego fluido deberíamos entonces realizar este cálculo un mínimo de 60 veces por segundo. Así, si tenemos 10.000 polígonos (algo bastante flojo hoy en día), podemos aproximar el tiempo requerido:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Tiempo por fotograma</th>
      <th>60Hz</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>O(1)</td>
      <td>0,1us</td>
      <td>6us</td>
    </tr>
    <tr>
      <td>O(log n)</td>
      <td>1,3us</td>
      <td>78us</td>
    </tr>
    <tr>
      <td>O(n)</td>
      <td>1ms</td>
      <td>60ms</td>
    </tr>
    <tr>
      <td>O(n log n)</td>
      <td>13,3ms</td>
      <td>798ms</td>
    </tr>
  </tbody>
</table>

<p>Vemos que el algoritmo O(n log n) se queda atrás ya que consume casi todo el tiempo disponible en un segundo, y aún quedan otras tareas por hacer (IA, renderizado, sonido, comunicaciones…). Pero es que aunque se pusiese en un hilo dedicado, la detección de colisiones suele ser un cálculo bloqueante de otras tareas, tales como interacción con objetos, recibir daño, restringir el movimiento. Así que incluso en el caso del O(n) estaríamos consumiendo el 6% de nuestro valioso tiempo en esto antes de poder proseguir con otros cálculos. Por último, suponer un escenario de sólo 10.000 polígonos es, hoy en día, hablar de un juego bastante sencillote. En entornos más exigentes (más de 1 millón de polígonos), la solución de orden lineal se mostraría inficiente también.</p>

<h2 id="complejidad-espacial">Complejidad espacial</h2>

<p>La tabla anterior mostró la eficiencia de ejecución de un algoritmo. A la hora de hablar de complejidad espacial, tenemos que hacer hincapié en que la gran mayoría de las veces se refiere al espacio requerido <em>por las estructuras auxiliares</em>, no por el conjunto de datos en sí que, obviamente, tendrá que contener los datos que necesite (dejaremos de lado técnicas de compresión o de control de redundancias).</p>

<p>Así pues, imaginemos que tenemos una colección de objectos de clase <code class="language-plaintext highlighter-rouge">C</code>, donde cada uno ocupa 20 bytes y, para simplificar, asumamos que la alineación de memoria es siempre perfecta. Dicha colección debe ser procesada por diversos algoritmos, cada uno con una complejidad espacial diferente (no pasaré de O(N<sup>2</sup>), ya que suele ser el peor caso asociado). Para ilustrar el caso haremos los cálculos suponiendo un <em>overhead</em> de un objeto auxiliar (20B):</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>1</th>
      <th>1.000</th>
      <th>1.000.000</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>O(1)</td>
      <td>20B</td>
      <td>20B</td>
      <td>20B</td>
    </tr>
    <tr>
      <td>O(log n)</td>
      <td>20B</td>
      <td>200B</td>
      <td>400B</td>
    </tr>
    <tr>
      <td>O(n)</td>
      <td>20B</td>
      <td>20KB</td>
      <td>20MB</td>
    </tr>
    <tr>
      <td>O(n log n)</td>
      <td>20B</td>
      <td>4MB</td>
      <td>8GB</td>
    </tr>
    <tr>
      <td>O(n<sup>2</sup>)</td>
      <td>20B</td>
      <td>20MB</td>
      <td>20PB</td>
    </tr>
  </tbody>
</table>

<p>Vemos claramente cómo no suelen ser viables algoritmos que requieren más de O(n) espacio adicional. Esto sin entrar en detalles tales como el tiempo que conlleva la reserva de memoria ni el patrón de accesos a todos los datos (caché).</p>

<h2 id="conclusiones">Conclusiones</h2>

<p>En esta primera entrega hemos expuesto las nociones de la complejidad algorítmica: notación O grande, complejidad temporal y espacial; y mostrado su impacto mediante ejemplos realistas.</p>

<p>Como guía rápida, en general debemos evitar cualquier algoritmo de orden cuadrático y superior en aquellos escenarios donde el conjunto de datos sea grande. En una entrega futura detallaremos la complejidad de algunos algoritmos conocidos así como diversas técnicas de optimización que podemos utilizar.</p>]]></content><author><name>Carlos Buchart</name></author><category term="algorithm" /><category term="performance" /><summary type="html"><![CDATA[Introducción al concepto de complejidad algorítmica, su impacto en el desarrollo y algunas consideraciones iniciales]]></summary></entry><entry><title type="html">Refactoring guiado por constantes en C++</title><link href="https://headerfiles.com/2023/06/30/refactoring-guiado-por-constantes" rel="alternate" type="text/html" title="Refactoring guiado por constantes en C++" /><published>2023-06-30T15:00:00+00:00</published><updated>2023-06-30T15:00:00+00:00</updated><id>https://headerfiles.com/2023/06/30/refactoring-guiado-por-constantes</id><content type="html" xml:base="https://headerfiles.com/2023/06/30/refactoring-guiado-por-constantes"><![CDATA[<p>En la <a href="/2023/03/27/que-conste-porque-construyo-con-constantes">última entrega</a> explicamos los beneficios del uso de constantes en nuestro código: mejoran la expresividad, dejan clara la intención de uso, ayudan a reducir errores y, en algunos casos, pueden mejorar el rendimiento del código.</p>

<p>En este artículo comentaremos un <em>refactoring</em> fácil y directo con el que podemos mejorar la limpieza y expresividad de nuestro código, y que podremos identificar fácilmente gracias al uso de constantes.</p>

<h2 id="inicialización-de-constantes">Inicialización de constantes</h2>

<p>La única <em>operación de escritura</em> permitida sobre una constante es su inicialización. Para ser claros, no debe confundirse con una asignación; la asignación modifica el valor de una variable ya existente, mientras que la inicialización dota a la variable (o constante en este caso) de su primer valor. Una vez inicializada, una constante no puede cambiar su valor nunca más.</p>

<p>Existen no pocas situaciones en las que nuestro código calcula un valor y luego, sin mutarlo, lo usa durante su ejecución. Casos como éstos son claros candidatos a convertirse en una constante (con la consecuente mejora del código).</p>

<p>Ahora bien, ¿qué ocurre si el valor de dicha constante se determina en varios pasos? Acá claramente necesitamos alterar el valor de la <em>constante</em> hasta que obtengamos su valor definitivo. Esto es bastante común en código antiguo (<em>legacy</em>). Este escenario también surge como consecuencia de un cambio que nos obliga a quitar el modificador <em>const</em> que ya teníamos para poder <em>arreglar un bug</em> o <em>incorporar una nueva característica</em>.</p>

<p>Por ejemplo, supongamos que tenemos una función para convertir una cadena de texto en un icono de 16px para un avatar (así, <em>HeaderFiles</em> generaría una imagen las letras <em>HF</em>). Como sabemos un poco de <em>clean code</em>, hemos extraído nuestras funciones y dejado claras las intenciones. Nuestro código es el siguiente:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Icon</span> <span class="nf">generate_icon_from_text</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">text</span><span class="p">,</span> <span class="kt">int32_t</span> <span class="n">width</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>

<span class="n">Icon</span> <span class="n">generate_avatar</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">text</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">constexpr</span> <span class="kt">int32_t</span> <span class="n">icon_width</span> <span class="o">=</span> <span class="mi">16</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">generate_icon_from_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">icon_width</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Después de la fase de pruebas, vemos que es necesario poder generar versiones del avatar para resoluciones HiDPI (1x: 16px, 2x: 32px, 3x: 48px). Esto nos obliga a cambiar el código un poco (me he inventado una API para determinar el modo HiDPI):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Icon</span> <span class="nf">generate_avatar</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">text</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">int32_t</span> <span class="n">icon_width</span> <span class="o">=</span> <span class="mi">16</span><span class="p">;</span>
    <span class="k">switch</span> <span class="p">(</span><span class="n">get_hidpi_mode</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="k">case</span> <span class="n">HiDPI_2x</span><span class="p">:</span> <span class="n">icon_width</span> <span class="o">=</span> <span class="mi">32</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
        <span class="k">case</span> <span class="n">HiDPI_3x</span><span class="p">:</span> <span class="n">icon_width</span> <span class="o">=</span> <span class="mi">48</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">generate_icon_from_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">icon_width</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Como vemos, para resolver el problema de las resoluciones hemos tenido que transformar nuestra constante (expresión constante realmente) en una variable mutable. Este patrón es un claro aviso de <em>refactoring</em>, ya que nos indica de zonas con una responsabilidad propia (en este caso, calcular el ancho del avatar) y que, por ende, pueden ser extraídas del código. Veamos algunas de las opciones de las que disponemos en C++ para ello.</p>

<h2 id="opciones-para-la-extracción-de-funciones-en-c">Opciones para la extracción de funciones en C++</h2>

<p>C++ proporciona diversos mecanismos para encapsular código, a saber:</p>

<ul>
  <li>Métodos miembro (en caso de que el código refactorizado sea una clase)</li>
  <li>Métodos estáticos</li>
  <li>Funciones globales (preferiblemente dentro de un <em>namespace</em>)</li>
  <li>Funciones locales (<em>namespace</em> anónimo)</li>
  <li>Funciones lambda</li>
</ul>

<p>Cuándo usar cada uno depende en gran medida de las circunstancias propias del código y de nuestras preferencias personales, aunque podemos trazar unas líneas generales de acción. Nótese que, si bien estamos aplicando estos mecanismos a la inicialización de constantes, son también válidos a cualquier escenario donde tengamos que elegir dónde ubicar una función.</p>

<ul>
  <li>
    <p>Si nuestra nueva función no va a ser reutilizada y el código es pequeño, podemos optar por una función lambda <em>in-place</em> (no es necesario darle nombre ya que la propia constante nos indica su razón de ser de forma expresiva):</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">Icon</span> <span class="nf">generate_avatar</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">text</span><span class="p">)</span>
  <span class="p">{</span>
      <span class="k">const</span> <span class="kt">int32_t</span> <span class="n">icon_width</span> <span class="o">=</span> <span class="p">[]</span> <span class="p">{</span>
          <span class="k">switch</span> <span class="p">(</span><span class="n">get_hidpi_mode</span><span class="p">())</span>
          <span class="p">{</span>
              <span class="k">case</span> <span class="n">HiDPI_2x</span><span class="p">:</span> <span class="k">return</span> <span class="mi">32</span><span class="p">;</span>
              <span class="k">case</span> <span class="n">HiDPI_3x</span><span class="p">:</span> <span class="k">return</span> <span class="mi">48</span><span class="p">;</span>
              <span class="nl">default:</span> <span class="k">return</span> <span class="mi">16</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}();</span>
      <span class="k">return</span> <span class="n">generate_icon_from_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">icon_width</span><span class="p">);</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Si la vamos a reutilizar dentro de una única función, y además necesitamos llamarla varias veces, podemos optar por una lambda con nombre, capturando los valores necesarios (nótese que no podremos acceder a miembros privados mediante este método).</p>
  </li>
  <li>
    <p>En caso de que la función sea algo más larga, no necesitemos <em>capturar</em> ningún valor y únicamente dependamos de los argumentos variables, usar una función local (en un <em>namespace</em> anónimo) es una mejor opción ya que reduce la extensión de la función inicial. Esta función puede definirse justo antes de la función que la usa, indicando así la relación que hay entre ambas.</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">namespace</span>
  <span class="p">{</span>
      <span class="kt">int32_t</span> <span class="n">get_avatar_width</span><span class="p">()</span>
      <span class="p">{</span>
          <span class="k">switch</span> <span class="p">(</span><span class="n">get_hidpi_mode</span><span class="p">())</span>
          <span class="p">{</span>
              <span class="k">case</span> <span class="n">HiDPI_2x</span><span class="p">:</span> <span class="k">return</span> <span class="mi">32</span><span class="p">;</span>
              <span class="k">case</span> <span class="n">HiDPI_3x</span><span class="p">:</span> <span class="k">return</span> <span class="mi">48</span><span class="p">;</span>
              <span class="nl">default:</span> <span class="k">return</span> <span class="mi">16</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
  <span class="p">}</span>

  <span class="n">Icon</span> <span class="n">generate_avatar</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">text</span><span class="p">)</span>
  <span class="p">{</span>
      <span class="k">const</span> <span class="k">auto</span> <span class="n">icon_width</span> <span class="o">=</span> <span class="n">get_avatar_width</span><span class="p">();</span>
      <span class="k">return</span> <span class="n">generate_icon_from_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">icon_width</span><span class="p">);</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Lo mismo ocurrirá cuando necesitemos reutilizar este código en varios puntos del mismo fichero: optaremos por una función local aunque en este caso puede ser conveniente ubicarla al principio del fichero.</p>
  </li>
  <li>
    <p>Si necesitamos usar miembros privados de la clase, ni las lambdas ni las funciones locales nos pueden ayudar, salvo que los pasemos como parámetros. Si son muchos argumentos a pasar, podemos optar por usar métodos privados constantes: tendrán un alcance a nivel de toda la clase y podremos acceder a todos los miembros. Por contrapartida los miembros privados son visibles al usuario de la clase (visibles en cuando legibles, no en cuanto a usables). Tradicionalmente la forma de evitar esto es mediante el <a href="https://cpppatterns.com/patterns/pimpl.html">patrón pImpl</a>.</p>
  </li>
  <li>
    <p>Por último, en caso de que veamos que la función extraida es reutilizable en más de un lugar, lo mejor será ubicarla en alguna posición global (biblioteca o módulo), preferiblemente dentro de un espacio de nombres. Si además de ser global, el método está estrechamente relacionado con una clase en específico, podremos situarlo como un método estático (un ejemplo claro de esto son funciones de creación de objetos).</p>
  </li>
</ul>

<h2 id="conclusiones">Conclusiones</h2>

<p>Hemos mostrado cómo el uso de constantes no sólo mejora la expresividad de nuestro código y nos proporciona mecanismos de seguridad ante errores humanos, sino que además puede indicarnos posibles <em>refactorings</em>. Tanto si nuestro código ya empleaba constantes, como si estamos comenzando a introducirlas, siempre nos serán útiles para detectar estos puntos de mejora.</p>]]></content><author><name>Carlos Buchart</name></author><category term="clean-code" /><category term="good-practices" /><category term="c++" /><summary type="html"><![CDATA[Continuamos estudiando el uso de constantes y explicamos un refactoring muy sencillo que podemos detectar gracias a ellas]]></summary></entry><entry><title type="html">Que conste porqué construyo con constantes</title><link href="https://headerfiles.com/2023/03/27/que-conste-porque-construyo-con-constantes" rel="alternate" type="text/html" title="Que conste porqué construyo con constantes" /><published>2023-03-27T07:00:00+00:00</published><updated>2023-03-27T07:00:00+00:00</updated><id>https://headerfiles.com/2023/03/27/que-conste-porque-construyo-con-constantes</id><content type="html" xml:base="https://headerfiles.com/2023/03/27/que-conste-porque-construyo-con-constantes"><![CDATA[<p>Esta semana un colega me preguntó cuáles eran las razones por las que, a la primera oportunidad, declaraba como constantes todas las variables posibles. Ello derivó en una interesante conversación que ha servido de inspiración para este artículo.</p>

<h2 id="constantes">Constantes</h2>

<p>Una constante es <em>un espacio de memoria con nombre cuyo valor no puede ser cambiado mientras el programa se ejecuta</em>. Son diferentes de los <em>literales</em>, que son datos presentados directamente en el código (tales como <code class="language-plaintext highlighter-rouge">42</code> y <code class="language-plaintext highlighter-rouge">"Hola mundo"</code>). Las constantes pueden ser de cualquier tipo: numéricas, cadenas de texto, booleanas, objetos, etc.</p>

<h2 id="constantes-en-c">Constantes en C++</h2>

<p>Primero que nada, vale la pena mencionar que existen lenguajes muy populares, como Python, que no soportan constantes como tal, aunque tengan una nomenclatura especial para referirse a ellas (<code class="language-plaintext highlighter-rouge">MAYÚSCULAS</code>).</p>

<p>C++ por otro lado, sí permite la definición de <em>variables no modificables</em>, es decir, que las constantes son iguales a las variables con la salvedad de que su valor puede asignarse una única vez (hablaríamos de una especie de <em>invariable</em>). En C++ hay cuatro formas de declarar una constante:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">#define RESPUESTA 42</code> (macro)</li>
  <li><code class="language-plaintext highlighter-rouge">const int respuesta = 42;</code> (constante en tiempo de compilación)</li>
  <li><code class="language-plaintext highlighter-rouge">const int respuesta = pregunta();</code> (constante en tiempo de ejecución)</li>
  <li><code class="language-plaintext highlighter-rouge">constexpr int respuesta = 42;</code> (expresión constante, a partir de C++11)</li>
</ul>

<p>Dejando de lado las macros, ya que no se recomienda su uso salvo para casos específicos (y eso que servidor era un adepto de las macros), los otros tres tipos podemos clasificarlos en dos categorías basándonos en qué momento la constante adquiere su valor: en tiempo de compilación o en tiempo de ejecución.</p>

<h2 id="uso-de-las-constantes">Uso de las constantes</h2>

<p>Discutiremos los diferentes usos de las constantes y sus beneficios (y contras cuando los haya) a partir de la clasificación dada anteriormente, además de algunos conceptos asociados.</p>

<h3 id="constantes-en-tiempo-de-compilación">Constantes en tiempo de compilación</h3>

<p>Las constantes en tiempo de compilación son inicializada con valores conocidos durante el propio proceso de compilación, bien mediante literales, directivas del preprocesador o expresiones constantes. Este tipo de constante sirve, en primer lugar, para darle un significado a un <em>valor mágico</em> que, de otro modo, necesitaría de información adicional para ser entendido. Por ejemplo, si vemos en el código 3.1415926 casi todo el mundo sabe que eso es Pi, pero si vemos un 12 no sabemos si se refiere a los meses del año, horas de un reloj, un límite de edad, etc. Otro uso similar es el de guardar algunas configuración específica de esa compilación (por ejemplo, tamaño del <em>stack</em> o la versión utilizada de una biblioteca).</p>

<p>Por otro lado, las constantes nos ayudan a no tener que repetir un valor. Así, tener una constante llamada <code class="language-plaintext highlighter-rouge">PI</code> es mucho más sencillo que escribir 3.1415926(…) cada dos por tres, además de arriesgarnos a escribirlo mal en algún momento.</p>

<p>Esto nos lleva al tercer uso de las constantes: tener una única fuente de verdad para ese valor. Además, si llegase a tener que modificarse en el código, sólo tendríamos que hacerlo en su definición, el resto de las referencias al mismo no tendrían que ser cambiadas.</p>

<h3 id="constantes-en-tiempo-de-ejecución">Constantes en tiempo de ejecución</h3>

<p>Las constantes cuyo valor no puede ser conocido durante el proceso de compilación, sino que dependen del estado actual del sistema al momento de ser inicializadas, se llaman constantes en tiempo de ejecución. Aún así, siguen siendo constantes, ya que una vez inicializadas no podemos cambiar su valor.</p>

<h4 id="constantes-globales-por-ejecución">Constantes globales por ejecución</h4>

<p>¿De qué nos sirve, pues, una constante cuyo valor no conocemos hasta el momento de ejecutarse? Lo primero y principal es precisamente establecer una regla de no modificación, de utilizar la semántica de declaración para impedir que cambie (intencionada o, más comúnmente, por error).</p>

<p>Pongamos el caso de un <em>feature flag</em>, de una opción de ejecución que se establece durante el arranque: el usuario puede asignar un valor u otro al iniciar el programa, pero una vez asignado no es posible cambiarlo a no ser que se reinicie. Esto puede ser, por ejemplo, el uso de aceleración por hardware para un motor de renderizado. Es fácil elegir uno u otro durante la inicialización, pero cambiarlo <em>en caliente</em> seguramente no compense el beneficio a la complejidad necesario de nuestro diseño de software. Así, una vez leído el parámetro, lo asignamos a una constante que no puede ser modificada.</p>

<h4 id="constantes-locales-y-clean-code">Constantes locales y <em>clean code</em></h4>

<p>De forma más local, si tenemos una variable cuyo valor no necesitamos modificar, ¿por qué vamos a dejar abierta esa posibilidad, la de alterar su valor y ocasionar un efecto inesperado? Supongamos el siguiente código:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">set_image_to_black</span><span class="p">(</span><span class="n">Image</span><span class="o">&amp;</span> <span class="n">image</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">bytes_per_row</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">width</span><span class="p">()</span> <span class="o">*</span> <span class="n">image</span><span class="p">.</span><span class="n">bpp</span><span class="p">()</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">height</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">height</span><span class="p">();</span>

    <span class="k">for</span> <span class="p">(</span><span class="k">auto</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">height</span><span class="p">;</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">auto</span> <span class="n">ptr</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">get_ptr_to_row</span><span class="p">(</span><span class="n">y</span><span class="p">);</span>
        <span class="n">memset</span><span class="p">(</span><span class="n">ptr</span><span class="p">,</span> <span class="n">bytes_per_row</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Es claro a partir de este código que todas las filas de la imagen tienen el mismo tamaño en bytes, que no varía. Además, dejamos al compilador la tarea de detectar cualquier intento de alteración de dicho valor. En resumen, dejamos claras nuestras intenciones.</p>

<p>Siguiendo con este punto, un dato local en una variable (en lugar de una constante) es una invitación a reutilizar dicho espacio de memoria para otros usos. Esto lleva a varios posibles problemas:</p>

<ul>
  <li>Uso inapropiado de un espacio con nombre para un fin diferente (reusar una variable <code class="language-plaintext highlighter-rouge">name</code> para guardar el <em>checksum</em> del fichero). Esto reduce la legibilidad del código.</li>
  <li>Apunta a un posible <em>refactoring</em> ya que claramente estamos teniendo bloques de diferente ámbito mezclados, y seguramente muy largos.</li>
  <li>Y el peor, podríamos introducir errores si quisiésemos volver a utilizar dicha variable con su sentido original. Esto también apuntaría a un <em>refactoring</em> ya que bien tenemos responsabilidades mezcladas, o el código es más largo del que podemos cubrir con ciertas garantías.</li>
</ul>

<h4 id="construyendo-constantinopla">Construyendo Constantinopla</h4>

<p>¿Y qué pasa con aquellas variables cuyo valor de asigna una única vez, pero no es posible conocer con certeza el valor dado que depende de muchos factores? Pongamos el siguiente ejemplo:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">draw_account_icon</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">row</span><span class="p">,</span> <span class="n">AccountType</span> <span class="n">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Color</span> <span class="n">color</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">AccountType</span><span class="o">::</span><span class="n">User</span> <span class="o">&amp;&amp;</span> <span class="n">row</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">::</span><span class="n">Blue</span><span class="p">;</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">AccountType</span><span class="o">::</span><span class="n">User</span> <span class="o">&amp;&amp;</span> <span class="n">row</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">::</span><span class="n">LightBlue</span><span class="p">;</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">AccountType</span><span class="o">::</span><span class="n">Group</span><span class="p">)</span> <span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">::</span><span class="n">Red</span><span class="p">;</span>
    <span class="k">else</span> <span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">::</span><span class="n">Green</span><span class="p">;</span>

    <span class="k">const</span> <span class="k">auto</span> <span class="n">icon</span> <span class="o">=</span> <span class="n">get_icon</span><span class="p">(</span><span class="n">type</span><span class="p">);</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">colorized_icon</span> <span class="o">=</span> <span class="n">colorize_icon</span><span class="p">(</span><span class="n">icon</span><span class="p">,</span> <span class="n">color</span><span class="p">);</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">y</span> <span class="o">=</span> <span class="n">row</span> <span class="o">*</span> <span class="n">colorized_icon</span><span class="p">.</span><span class="n">get_height</span><span class="p">();</span>
    <span class="n">draw_icon</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">colorized_icon</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Éste quizás es uno de los argumentos tácitos más comunes para no declarar como constante una variable. En la mayoría de los casos esto es también un indicativo de que nuestro código está haciendo demasiadas cosas y que deberíamos refactorizar. Así, podríamos extraer una función que, dado el tipo de cuenta y la fila en la que ha de ser presentada, devuelve el color del icono asociado.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Color</span> <span class="nf">get_color_for_account</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">row</span><span class="p">,</span> <span class="n">AccountType</span> <span class="n">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">AccountType</span><span class="o">::</span><span class="n">User</span> <span class="o">&amp;&amp;</span> <span class="n">row</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="n">Color</span><span class="o">::</span><span class="n">Blue</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">AccountType</span><span class="o">::</span><span class="n">User</span> <span class="o">&amp;&amp;</span> <span class="n">row</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="n">Color</span><span class="o">::</span><span class="n">LightBlue</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">AccountType</span><span class="o">::</span><span class="n">Group</span><span class="p">)</span> <span class="k">return</span> <span class="n">Color</span><span class="o">::</span><span class="n">Red</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">color</span> <span class="o">=</span> <span class="n">Color</span><span class="o">::</span><span class="n">Green</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">draw_account_icon</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">row</span><span class="p">,</span> <span class="n">AccountType</span> <span class="n">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">icon</span> <span class="o">=</span> <span class="n">get_icon</span><span class="p">(</span><span class="n">type</span><span class="p">);</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">color</span> <span class="o">=</span> <span class="n">get_color_for_account</span><span class="p">(</span><span class="n">row</span><span class="p">,</span> <span class="n">type</span><span class="p">);</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">colorized_icon</span> <span class="o">=</span> <span class="n">colorize_icon</span><span class="p">(</span><span class="n">icon</span><span class="p">,</span> <span class="n">color</span><span class="p">);</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">y</span> <span class="o">=</span> <span class="n">row</span> <span class="o">*</span> <span class="n">colorized_icon</span><span class="p">.</span><span class="n">get_height</span><span class="p">();</span>
    <span class="n">draw_icon</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">colorized_icon</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="métodos-constantes">Métodos constantes</h3>

<p>Otro uso de objetos constantes (tanto en tiempo de compilación como especialmente en tiempo de ejecución), es la de limitar el acceso a los métodos que se pueden llamar. Un método puede ser marcado como <code class="language-plaintext highlighter-rouge">const</code>, de forma que se establece un contrato mediante el cual se <em>promete</em> que dicho método no modifica el estado del objeto. Como es lógico, no es posible llamar a métodos no-const desde un objeto marcado como constante (y esto incluye a los operadores de asignación).</p>

<p>Siguiendo con la lógica del punto anterior, si un método no modifica el estado del objeto, ¿por qué voy a querer marcarlo como que sí lo hace? Respuestas como “por si acaso” o “igual en el futuro sí” demuestran simplemente un diseño pobre y poco pensado. Además, si los requerimientos cambian en el futuro también lo puede hacer la API de la clase, y en este caso incluso tendremos ayuda ya que nuestro método que antes era const y ahora no lo es no podrá ser llamado desde los objetos que habíamos también declarado como constantes, por lo que el compilador nos servirá de guía para revisar nuestro código después de la modificación y evitar efectos indeseados.</p>

<p>Por otro lado, C++ tiene <em>puertas traseras</em> en el diseño de los métodos const que son necesario conocer.</p>

<ul>
  <li>El modificador <code class="language-plaintext highlighter-rouge">mutable</code> indica que la variable miembro asociada puede ser modificada desde un método const. Obviamente abusar de este método es falsear el contrato establecido. Recordad que C++ nos hace difícil dispararnos en el pie, pero cuando lo logramos nos volamos la pierna entera (<a href="https://www.goodreads.com/quotes/226222-c-makes-it-easy-to-shoot-yourself-in-the-foot">Bjarne Stroustrup</a>). Seguramente el uso más común de este modificador es para declarar <code class="language-plaintext highlighter-rouge">mutex</code> u otras estructuras para proteger secciones críticas, ya que se deberían poder usar en métodos tipo <em>get</em> (que normalmente son constantes), pero obviamente el mutex debe poder modificar su estado para ello. De todas formas, estos casos son excepcionales ya que el propio mutex garantiza su coherencia.</li>
  <li>Uso de punteros inteligentes. En estos casos no es posible modificar el puntero inteligente desde el método const, <em>pero sí el objeto al que apunta</em>. Esto permite llamar a métodos no-const en objetos referenciados desde punteros inteligentes. Esto no ocurre con los punteros normales (<em>raw</em>).</li>
  <li>El modificador <code class="language-plaintext highlighter-rouge">const</code> no impide modificar variables globales, o llamar a métodos estáticos que sí puedan modificar el estado del sistema.</li>
  <li>El operador <code class="language-plaintext highlighter-rouge">const_cast</code> que permite <em>quitar</em> el modificar const a un objeto. Aunque tiene sus casos de uso, la regla general es evitarlo.</li>
</ul>

<p>Los métodos const son, dentro las limitaciones anteriores, un indicativo de métodos de <em>sólo lectura</em>. Esto permite identificar más fácilmente problemas de sincronización del estilo “escritores - lectores”.</p>

<p>C++ permite, además, realizar una sobrecarga de métodos con versiones <code class="language-plaintext highlighter-rouge">const</code> y no-<code class="language-plaintext highlighter-rouge">const</code>. Por ejemplo, la versión const pod–ría devolver una referencia constante a una variable miembro mientras que la no-const devolvería una copia. Si declaramos nuestro objeto como <code class="language-plaintext highlighter-rouge">const</code> estaremos dirigiendo al compilador a la versión optimizada del método.</p>

<p>En resumen, definiendo nuestras variables como <code class="language-plaintext highlighter-rouge">const</code> dejamos al compilador la tarea de filtrar qué operaciones son posibles además de permitir ciertas optimizaciones en el proceso.</p>

<p>Por último, y casi nota al margen, si un método no modifica a miembros de la clase, pero tampoco los usa, es muy probable que estemos ante un posible método estático, o que debería ser movido a una biblioteca o módulo separado. Además, si dicho método sólo se usa dentro de una determinada implementación, igual lo mejor es moverlo a una función local (en un <code class="language-plaintext highlighter-rouge">namespace</code> anónimo) o por lo menos como parte de otro fichero. Con esto limpiamos la interfaz de las clase, además de reducir (muy ligeramente) el tiempo de compilación.</p>

<h3 id="constexpr-vs-const"><code class="language-plaintext highlighter-rouge">constexpr</code> vs <code class="language-plaintext highlighter-rouge">const</code></h3>

<p>En C++11 se introdujo un nuevo tipo de constante en tiempo de compilación, llamado <code class="language-plaintext highlighter-rouge">constexpr</code>. La idea es que el compilador puede hacer uso de estas constantes y evaluarlas durante la generación del binario para producir código optimizado (aunque no es obligatorio). Además, es posible definir funciones <code class="language-plaintext highlighter-rouge">constexpr</code> que son evaluables en tiempo de compilación, aunque tienen algunas limitaciones dependiendo de la versión de C++ que se use.</p>

<p>Definir, si se puede, una constante como <code class="language-plaintext highlighter-rouge">constexpr</code> abre las puertas a posibles optimizaciones, además de dejar más clara la intención de definir una constante en tiempo de compilación.</p>

<h4 id="funciones-constexpr-y-consteval">Funciones <code class="language-plaintext highlighter-rouge">constexpr</code> y <code class="language-plaintext highlighter-rouge">consteval</code></h4>

<p>Como se dijo antes, las funciones marcadas como <code class="language-plaintext highlighter-rouge">constexpr</code> <em>pueden</em> ser evaluadas en tiempo de compilación. Lo harán si el resultado se necesita en dicho momento, como por ejemplo para calcular el tamaño de un arreglo, pero es posible que otras llamadas se difieran al momento de ejecución. Las funciones marcadas como <code class="language-plaintext highlighter-rouge">consteval</code> (C++20), son evaluadas <em>únicamente</em> en tiempo de compilación. No existen variables <code class="language-plaintext highlighter-rouge">consteval</code> ya que su uso estaba cubierto por completo con <code class="language-plaintext highlighter-rouge">constexpr</code> en la especificación de C++11.</p>

<h3 id="argumentos-const">Argumentos <code class="language-plaintext highlighter-rouge">const</code></h3>

<p>Seguramente este punto sea ampliamnte conocido por el lector más veterano, ya que data de la época del C++ <em>viejo</em>. Básicamente se trata de definir los argumentos de una función, cuando son objetos, como referencias constantes, a fin de evitar copias innecesarias. Como ejemplo (<code class="language-plaintext highlighter-rouge">std::string trim(const std::string&amp; str)</code>). Esto además permite el uso de dichas funciones sobre objetos construidos implícitamente a partir de literales (<code class="language-plaintext highlighter-rouge">const auto trimmed = trim("   hola mundo  ");</code>). Desde C++11 existen pequeñas variantes de esta <em>regla universal</em> en lo que se refiere a los constructores de movimiento, pero no profundizaré en dicha explicación ahora (para más información consultar Effective Modern C++, de Scott Meyers, Item 41).</p>

<h3 id="miembros-constantes">Miembros constantes</h3>

<p>Las clases pueden tener miembros constantes que pueden ser inicializados únicamente en los constructores. Como puede deducirse si se piensa un poco, esto imposibilita el uso del operador de asignación por defecto, ya que éste básicamente lo que hace es llamar al operador de asignación de los miembros de la clase, y a una constante no se le puede volver a dar un valor. Esta limitación puede eludirse definiendo nuestro propio operador de asignación que <em>salte</em> las constantes (aunque tendremos que mirar que la clase entonces quede en un estado coherente).</p>

<h3 id="alternativas-a-constantes">Alternativas a constantes</h3>

<p>Algunas veces no es posible utilizar una constante como tal, pero al menos podemos definir un mecanismo que nos alerte de <em>reinicializaciones</em>. Se trata básicamente de usar un método <code class="language-plaintext highlighter-rouge">get</code> con una bandera de inicialización que se levanta con la primera llamada al <code class="language-plaintext highlighter-rouge">set</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="k">class</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="k">class</span> <span class="nc">RuntimeConstant</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">m_value</span><span class="p">;</span>

<span class="nl">public:</span>
    <span class="kt">void</span> <span class="n">set</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span><span class="o">&amp;</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">assert</span><span class="p">(</span><span class="o">!</span><span class="n">m_value</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">m_value</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">runtime_error</span><span class="p">(</span><span class="s">"Re-initialization detected"</span><span class="p">);</span> <span class="c1">// no further information for simplicity</span>
        <span class="p">}</span>
        <span class="n">m_value</span> <span class="o">=</span> <span class="n">value</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">T</span> <span class="n">get</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span>
        <span class="n">assert</span><span class="p">(</span><span class="n">m_value</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">m_value</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">runtime_error</span><span class="p">(</span><span class="s">"Uninitialized run-time constant"</span><span class="p">);</span> <span class="c1">// no further information for simplicity</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">m_value</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<h2 id="conclusiones">Conclusiones</h2>

<p>Como hemos visto, el uso del modificador <code class="language-plaintext highlighter-rouge">const</code> (y <code class="language-plaintext highlighter-rouge">constexpr</code>) no se restringe únicamente a dar nombre a valores mágicos, sino que además mejora la expresividad del código, limita los posibles errores y abusos, ayuda a detectar zonas de mejora (especialmente extracción de funciones) y permite al compilador realizar algunas optimizaciones.</p>]]></content><author><name>Carlos Buchart</name></author><category term="clean-code" /><category term="good-practices" /><category term="c++" /><summary type="html"><![CDATA[Enumeramos y razonamos los motivos que me llevan a usar el modificador 'const' en cada momento que puedo]]></summary></entry><entry><title type="html">Cómo cambiar una bombilla</title><link href="https://headerfiles.com/2023/02/21/how-to-change-a-light-bulb" rel="alternate" type="text/html" title="Cómo cambiar una bombilla" /><published>2023-02-21T07:00:00+00:00</published><updated>2023-02-21T07:00:00+00:00</updated><id>https://headerfiles.com/2023/02/21/how-to-change-a-light-bulb</id><content type="html" xml:base="https://headerfiles.com/2023/02/21/how-to-change-a-light-bulb"><![CDATA[<p>Llevo más de 20 años desarrollando software y durante muchos otros he impartido o colaborado en diversas asignaturas relacionadas con la programación: Informática I (en diversas modalidades, pero siempre como ayudante), Diseño de Sistemas Operativos (tanto en Venezuela como en España), y Seguridad de Redes.</p>

<p>En todas ellas he visto el mismo patrón: la mayoría de los estudiantes (incluso algunos de los <em>brillantes</em>) les costaba pasar de un simple <em>caletreo</em> en lo que a programación se refería: aprendían muy bien los conceptos teóricos de las instrucciones de control de flujo, sabían lo que estaban haciendo los programas que veíamos en clase y muchas veces salían de los atolladeros de errores de compilación de C++ por cuenta propia. Pero cuando tocaba realizar un programa desde cero o incluso modificar (sustancialmente) un programa dado, no hacían más que comenzar a poner bucles “for” acá y allá sin razón, o a preguntar si debían usar un “if” o una función. Parecía que todo lo demás hubiese sido una farsa. Con el tiempo he llegado a ver ese comportamiento no sólo en alumnos, sino en “profesionales” del sector.</p>

<p>Después de muchas reflexiones y de comentarlo con colegas de la academia y de la industria, he concluido que el problema radica en que se han saltado un paso en su formación. Me explico. Cuando entré en la facultad me di cuenta de que había <em>algo raro</em>, y que además le pasaba a casi la totalidad de los que llevaban un tiempo programando por cuenta propia. Pasados unos meses, noté que <em>eso</em> se apoderaba de todos mis compañeros de estudios. Los que más <em>contagiados</em> estaban solían ser los que lograban que sus proyectos funcionasen más rápidamente, los que destacaban en los maratones de programación. Y lo mismo he observado con el tiempo en otras escuelas de informática y en las diferentes empresas por donde he pasado.</p>

<p>Pero, ¿qué era <em>eso</em> que se propagaba como una epidemia? Creo que cualquiera que haya tenido un mínimo trato con un desarrollador de software lo ha podido <em>oler</em> y me sabrá entender. Sencillamente nuestro cerebro estaba sufriendo un daño irreparable, permanente y significativamente visible; y no, no es que no pudiésemos pensar, es que lo hacíamos diferente, ya no como un ser humano, sino como una máquina.</p>

<h2 id="cambiar-una-bombilla">Cambiar una bombilla</h2>

<p>Había un ejercicio que se solía proponer en muchos cursos de Algoritmos I y, que si bien tiene sus variantes, en esencia es el mismo. Digo <em>solía</em> porque hasta donde he visto ya no se expone en muchas facultades ni cursos de programación. Lo dejaré escrito y daré unos momentos para que reflexionen sobre ello:</p>

<p><em>Diseñe un algoritmo para cambiar una bombilla.</em> (Para los no <em>iniciados</em>, un algoritmo es un conjunto de pasos para hacer algo, el plan de trabajo).</p>

<p>⌛️ Tiempo de reflexión…</p>

<p>Muy bien. A ver vuestros trabajos, veamos, tomemos el primero que tenemos acá:</p>

<ol>
  <li>Comprar bombilla nueva</li>
  <li>Poner una escalera debajo de la lámpara</li>
  <li>Subir la escalera</li>
  <li>Desenroscar bombilla vieja</li>
  <li>Enroscar bombilla nueva</li>
  <li>Bajar escalera</li>
  <li>Tirar bombilla vieja</li>
  <li>Guardar escalera</li>
</ol>

<h3 id="revisión">Revisión</h3>

<p>Bien, ahora veamos lo que podría decir un ordenador sobre la línea 2</p>

<ul>
  <li>Ordenador: Fenomenal, ¡gracias! a ver ¿qué es escalera?</li>
  <li>Programador: Una escalera es un conjunto de peldaños o escalones que enlazan dos planos a distinto nivel, y que sirven para subir y bajar.</li>
  <li>O: Vale, ¿qué es peldaño?</li>
  <li>P: Un peldaño es un trozo de madera, hierro, plástico, cemento, en el que se apoya el pie para subir o bajar.</li>
  <li>O: Muy bien, ¿qué es subir? ¿qué es madera? ¿qué es hierro? ¿que es bajar? ¿qué es pie?…</li>
</ul>

<p>¿Y sobre la línea 5?</p>

<ul>
  <li>O: ¡Me encanta! Antes de seguir, ¿me explicas qué es eso de enroscar?</li>
  <li>P (ya en alerta después de la experiencia con la línea 3): Consiste en cuatro pasos: primero sujetar la bombilla con la mano dominante con la fuerza suficiente para que no se caiga y que podamos vencer el rozamiento de la rosca en el sócate, pero sin ser demasiada como para romperla y hacernos daño; segundo, ubicar la rosca de la bombilla en la entrada del sócate; tercero, realizar un movimiento repetitivo de unos 170° cada uno en dirección antihoraria de la bombilla (ayudarse con la otra mano mientras la bombilla aún no esté sujeta por el sócate); cuarto, repetir el paso tres hasta que la bombilla esté firme en el sócate.</li>
  <li>O: ¡Estupendo! ¿Qué es un sócate?</li>
  <li>P: 😒😒😒</li>
</ul>

<p>Y así podríamos continuar hasta que el ordenador ya lo tuviera todo claro. Veríamos entonces que nuestro algoritmo es realmente un tratado completo acerca de la anatomía de la mano y el brazo, de la estructura de una bombilla y de la lámpara, un inventario de herramientas y utensilios, y toda una orquesta de movimientos humanos de sujeción y desplazamiento, por no decir un glosario de los términos más básicos que cualquier niño de 3 años conoce.</p>

<h2 id="el-tonto-más-rápido-del-condado">El tonto más rápido del condado</h2>

<p>Creo que queda claro el punto nuclear: el ordenador no es más que una pieza tonta de silicio al que hay que explicárselo todo. Eso sí, es el tonto más rápido del lugar. De la misma forma que nuestro ejemplo anterior, el más simple programa de ordenador puede terminar siendo bastante complejo desde el punto de vista del usuario.</p>

<p>Cuando uno empieza a programar descubre que uno tiene el poder de hacer que el ordenador haga lo que uno quiera, que sólo protestará en la medida de si puede hacerlo o no, pero no tendrá pereza, ni dirá que ya ha hecho mucho, ni criticará la decisión que uno ha tomado y, si uno ha metido la pata, el ordenador no dirá nada y lo hará, siendo uno el responsable de ello. De hecho, se suele decir que los ordenadores siguen un modelo GIGO (<em>garbage in, garbage out</em>): si les damos la orden correcta, harán lo que uno pretendía, pero si uno da la orden equivocada, el ordenador no hará lo que uno quería. El ordenador no tiene <em>telepatía</em>, sólo sigue órdenes concretas y precisas.</p>

<h2 id="evolucionando">Evolucionando</h2>

<p>El día que un aspirante a desarrollador cae en la cuenta de todo esto, automáticamente se hace mejor, ¡evoluciona!, ya que entenderá que no debe esperar ni por asomo que el ordenador haga mágicamente lo que él quería, sino que sabrá que debe dar todas y cada una de las instrucciones de una forma detallada y ordenada. Su mente dejará de funcionar como la de un humano provisto de un alma inteligente y libre, con experiencia, iniciativa, curiosidad, y empezará a contar ciclos de reloj, a no asumir nada, a no dar nada por sabido de antemano, a ser muy explícito y cuadriculado.</p>

<p>En estos últimos días hemos sido testigos del gran avance en materias de <em>deep learning</em>, con los modelos de procesamiento de lenguaje GPT-3 (y pronto GPT-4), generación de imágenes <em>stable diffusion</em>, y su aplicación en prácticamente cualquier ámbito profesional y artístico. Además, desde hace años incluso los ordenadores más sencillos cuentan con una potencia de cálculo bastante superior a la de un cerebro humano. Cada segundo se procesa una cantidad inimaginable de datos. Las herramientas cada vez hacen más cosas que antes hacían las personas (bueno, es lo que ha pasado siempre desde la invención de la rueda y la palanca, la domesticación de caballos, el motor de vapor, la electrónica y así hasta la IA). Hay quienes ven amenazas, otros oportunidades, otros un cambio de paradigma.</p>

<p>Pero incluso con todo esto, el ordenador no ha cambiado en sus fundamentos: no piensa, no tiene voluntad, no es libre, sólo sigue instrucciones, aunque éstas sean complejísimas, se nutran de toda la información mundial y se retroalimenten continuamente.</p>

<p>El tonto del condado es cada vez más rápido y tiene mejores instrucciones y datos sobre los que trabajar, pero sigue siendo el tonto y necesita de seres racionales -personas- que entiendan esto y que puedan <em>pensar</em> (procesar sería una mejor palabra) como lo hace un ordenador para poder progresar.</p>

<p><em>Pensar como un ordenador</em> es, a su vez, un término que varía con el tiempo en el cómo, mas no en el qué: ya lo hizo del paso de ensamblador a lenguajes de alto nivel y luego a las aplicaciones web y móviles, lo hizo durante el cambio de programación mono-hilo a software altamente concurrente, de las tarjetas perforadas a las interfaces gráficas y a la realidad aumentada / virtual. Pero siempre necesitaremos saber que el ordenador no es más que eso, una máquina de cómputo, por muy rápida y compleja que sea.</p>

<h2 id="encendamos-la-luz">Encendamos la luz</h2>

<p>Volvamos al ejercicio inicial y dediquemos unos momentos a pensar cómo le explicaríamos a un ordenador que cambie una bombilla, sin asumir nada, sin dejar cabos sueltos… Es un ejercicio sin fin, y es su razón de ser. Realmente pienso que si este ejercicio se volviese a exponer en los cursos de programación veríamos un cambio sustancial de calidad; y que, independientemente del lenguaje de desarrollo, <em>framework</em>, tecnología, entenderíamos que no hay magia, no hay intuición, no hay libre albedrío en la informática, sólo instrucciones explícitas, sin dobles sentidos, con todos los datos, lógicos, binarios (hace una cosa o no la hace).</p>]]></content><author><name>Carlos Buchart</name></author><category term="algorithm" /><category term="education" /><category term="opinion" /><summary type="html"><![CDATA[¿Qué echo en falta en muchos cursos de programación y no cambia nada incluso con los últimos progresos de la IA?]]></summary></entry><entry><title type="html">Me gusta el mueve mueve</title><link href="https://headerfiles.com/2023/01/29/i-like-to-move-it" rel="alternate" type="text/html" title="Me gusta el mueve mueve" /><published>2023-01-29T22:00:00+00:00</published><updated>2023-01-29T22:00:00+00:00</updated><id>https://headerfiles.com/2023/01/29/i-like-to-move-it</id><content type="html" xml:base="https://headerfiles.com/2023/01/29/i-like-to-move-it"><![CDATA[<p>Cuando se presentó C++11 hace más de 12 años, los amantes de C++ vimos cómo comenzaba una nueva era para el lenguaje, una <em>modernización</em> del mismo, y nos hizo tener que volver a estudiarlo (si es que alguien deja de hacerlo con C++), con ahora clásicos como el <a href="https://www.oreilly.com/library/view/effective-modern-c/9781491908419/">“Effective Modern C++” (Scott Meyers)</a>.</p>

<p>C++11 introdujo un montón de nuevas características, tales como <em>templates variádicos</em>, <em>range-for</em>, inicializadores de listas, inferencias de tipos (<code class="language-plaintext highlighter-rouge">auto</code>), constante nula real (<code class="language-plaintext highlighter-rouge">nullptr</code>), enumeraciones de tipo estricto (<code class="language-plaintext highlighter-rouge">enum class</code>), nuevos literales, multitarea (hilos, mutex), <code class="language-plaintext highlighter-rouge">static_assert</code>, <code class="language-plaintext highlighter-rouge">constexpr</code>, r-values, semántica de movimiento, funciones lambda, herencia de constructores, punteros inteligentes, especificadores de herencia <code class="language-plaintext highlighter-rouge">override</code> y <code class="language-plaintext highlighter-rouge">final</code>, expresiones regulares, tipos de enteros de tamaño fijo (<code class="language-plaintext highlighter-rouge">int32_t</code>, <code class="language-plaintext highlighter-rouge">uint8_t</code>, …), generadores de números aleatorios extensibles y <em>type traits</em>, entre tantos otros.</p>

<p>Como se ve, esta versión trajo multitud de mejoras tanto en su núcleo como en la biblioteca estándar, no sólo poniendo al día al lenguaje sino sentando las bases para futuras actualizaciones, que no ha parado desde entonces (se presentan nuevas versiones cada 3 años: C++14, C++17, C++20 y próximamente C++23).</p>

<p>Volviendo a la lista anterior, de entre todas las incorporaciones, una de las menos entendidas es la semántica de movimiento, no por su complejidad sino por confusión que genera, especialmente en los que recién comienzan a usar el <em>C++ moderno</em>. Veamos un poco de qué va eso del <code class="language-plaintext highlighter-rouge">move</code>.</p>

<h2 id="referencias-rvalue">Referencias rvalue</h2>

<p>Primero decir que un <em>lvalue</em> es una expresión con nombre, a la que se le puede asignar un valor. Se llaman así porque suelen aparecer a la izquierda (<em>left</em>) de una asignación. Así, tenemos además referencias a lvalue (<code class="language-plaintext highlighter-rouge">T&amp;</code>) y referencias constantes a lvalue (<code class="language-plaintext highlighter-rouge">const T&amp;</code>, o <code class="language-plaintext highlighter-rouge">T const&amp;</code> para los <em>east-const</em>).</p>

<p>Por el contrario, un <em>rvalue</em> es un temporal, un <em>sin nombre</em>, al que no se le puede asignar un valor. Lo que C++11 introduce entonces es el concepto de referencia a rvalue, con la sintaxis <code class="language-plaintext highlighter-rouge">T&amp;&amp;</code>. El punto central de todo esto está en que una referencia a rvalue puede ser modificada, sólo que como lo que se modifica es un rvalue, es decir, un temporal, podemos aprovecharnos de eso para hacer grandes optimizaciones.</p>

<h3 id="ejemplos">Ejemplos</h3>

<table>
  <thead>
    <tr>
      <th>Expresión</th>
      <th>Tipo</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a=1</code></td>
      <td><code class="language-plaintext highlighter-rouge">a</code> es lvalue, <code class="language-plaintext highlighter-rouge">1</code> es una constante</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a=b</code></td>
      <td><code class="language-plaintext highlighter-rouge">a</code> y <code class="language-plaintext highlighter-rouge">b</code> son lvalue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">foo()</code></td>
      <td>El objeto devuelto por <code class="language-plaintext highlighter-rouge">foo()</code> es un rvalue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a+b</code></td>
      <td>r-value</td>
    </tr>
  </tbody>
</table>

<h3 id="stdmove"><code class="language-plaintext highlighter-rouge">std::move</code></h3>

<p>Antes de proseguir, es importante comentar el segundo caso, donde aunque <code class="language-plaintext highlighter-rouge">b</code> está a la “derecha” de la igualdad, no es un rvalue, ya que (digamos) no es un <em>temporal</em>.</p>

<p>Con la función <a href="https://es.cppreference.com/w/cpp/utility/move"><code class="language-plaintext highlighter-rouge">std::move</code></a> podemos convertir una referencia a lvalue en una referencia a rvalue (si la referencia ya es a rvalue, no hay cambios). Nótese que esto no es más que una forma de forzar tipos de cara al compilador: <code class="language-plaintext highlighter-rouge">std::move</code> no tiene coste alguno a nivel de ejecución. De hecho, veremos, citando a Mayers, que <code class="language-plaintext highlighter-rouge">std::move</code> <em>no mueve nada</em>.</p>

<h2 id="constructores-de-movimiento">Constructores de movimiento</h2>

<p>Así como en C++03 teníamos el constructor de copia (que recibe una referencia constante a lvalue, <code class="language-plaintext highlighter-rouge">const T&amp;</code>), en C++11 se introduce el constructor de movimiento, que recibe una referencia a rvalue (<code class="language-plaintext highlighter-rouge">T&amp;&amp;</code>).</p>

<p>Así, una expresión como</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="s">"foo"</span><span class="p">;</span> <span class="p">}</span>

<span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">bar</span><span class="p">{</span><span class="n">foo</span><span class="p">()};</span>
</code></pre></div></div>

<p>llamaría al constructor de movimiento en lugar del de copia, porque <code class="language-plaintext highlighter-rouge">foo()</code> se interpreta como una referencia a rvalue.</p>

<p>Lo anterior parece una tontería, pero permite construir un objeto sacando partido de que sabemos que el argumento que recibmos es un temporal. Un ejemplo típico es el de los contenedores:</p>

<p>Tomemos como ejemplo un contenedor básico:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="k">class</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="k">class</span> <span class="nc">MyVector</span> <span class="p">{</span>
    <span class="n">T</span><span class="o">*</span> <span class="n">m_data</span><span class="p">{</span><span class="nb">nullptr</span><span class="p">};</span>
    <span class="kt">size_t</span> <span class="n">m_size</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span>

<span class="nl">public:</span>
    <span class="o">~</span><span class="n">MyVector</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">delete</span><span class="p">[]</span> <span class="n">m_data</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">explicit</span> <span class="n">MyVector</span><span class="p">(</span><span class="k">const</span> <span class="n">MyVector</span><span class="o">&amp;</span> <span class="n">o</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">m_size</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">try</span> <span class="p">{</span>
                <span class="n">m_data</span> <span class="o">=</span> <span class="k">new</span> <span class="n">T</span><span class="p">[</span><span class="n">o</span><span class="p">.</span><span class="n">m_size</span><span class="p">];</span>
                <span class="n">m_size</span> <span class="o">=</span> <span class="n">o</span><span class="p">.</span><span class="n">m_size</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">ii</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">ii</span> <span class="o">&lt;</span> <span class="n">m_size</span><span class="p">;</span> <span class="o">++</span><span class="n">ii</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">m_data</span><span class="p">[</span><span class="n">ii</span><span class="p">]</span> <span class="o">=</span> <span class="n">o</span><span class="p">.</span><span class="n">m_data</span><span class="p">[</span><span class="n">ii</span><span class="p">];</span>
                <span class="p">}</span>
            <span class="p">}</span> <span class="k">catch</span> <span class="p">(...)</span> <span class="p">{</span>
                <span class="k">delete</span><span class="p">[]</span> <span class="n">m_data</span><span class="p">;</span>
                <span class="n">m_size</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>El constructor de copia tradicional (C++03) debería reservar por lo menos la misma cantidad de memoria que el vector de origen, y posteriormente copiar todos los elementos. Puede verse que ésta es una operación que tiene un coste, y dependiendo del tamaño del contenedor, éste puede ser alto. Si a esto añadimos que el argumento es un objeto temporal, tenemos que contar entonces con el destructor del objeto temporal y el hecho de que durante un tiempo hemos duplicado el consumo de memoria de esa función.</p>

<p>Un constructor de movimiento sabría que el objeto que recibe será destruido inmediatamente después (o por lo menos no se espera que siga siendo válido), por lo que podría, en lugar de reservar un nuevo bloque de memoria y copiar los elementos, simplemente intercambiar el puntero del nuevo objeto con el del temporal. Esto convierte una operación de orden lineal a una de orden constante (el sueño de todo optimizador). Además, el destructor del temporal sería una operación muy simple, ya que llamaría a un <code class="language-plaintext highlighter-rouge">delete[] nullptr</code>, que como sabemos no hace nada (y es legal, para los que no lo supiesen). Nuestro ejemplo anterior podría lucir así después de añadir un constructor de movimiento trivial:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="k">class</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="k">class</span> <span class="nc">MyVector</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="c1">// ...</span>
    <span class="k">explicit</span> <span class="n">MyVector</span><span class="p">(</span><span class="n">MyVector</span><span class="o">&amp;&amp;</span> <span class="n">o</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">swap</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">m_data</span><span class="p">,</span> <span class="n">m_data</span><span class="p">);</span>
        <span class="n">std</span><span class="o">::</span><span class="n">swap</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">m_size</span><span class="p">,</span> <span class="n">m_size</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Nótese el uso de <a href="https://es.cppreference.com/w/cpp/algorithm/swap"><code class="language-plaintext highlighter-rouge">std::swap</code></a>; esto es debido a que el objeto pasado como referencia a rvalue aún existe y debe ser destruido al finalizar su tiempo de vida, por lo que si simplemente copiamos el puntero en <code class="language-plaintext highlighter-rouge">o.m_data</code> nos quedaríamos con un <em>dangling pointer</em> que llevaría a una violación de segmento al primer intento de acceso. No, debemos asegurarnos que el rvalue queda en un estado consistente y que su destrucción no afecte al objeto construido con él.</p>

<p>Como podemos imaginar de todo lo anterior, la diferencia de rendimiento es enorme, tal y como ejemplifica <a href="https://quick-bench.com/q/WJUP1kfcKItGffdDWtG9Ly31_40">este benchmarking</a> donde se compara la copia y el movimiento de un <code class="language-plaintext highlighter-rouge">std::vector</code> de 100.000 enteros (adjunto el código resumido):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">constexpr</span> <span class="kt">size_t</span> <span class="n">N</span><span class="p">{</span><span class="mi">100'000</span><span class="p">};</span>

<span class="kt">void</span> <span class="n">CopyVector</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">v</span><span class="p">(</span><span class="n">N</span><span class="p">);</span>

    <span class="k">auto</span> <span class="n">w</span> <span class="o">=</span> <span class="n">v</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">MoveVector</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">v</span><span class="p">(</span><span class="n">N</span><span class="p">);</span>

    <span class="k">auto</span> <span class="n">w</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">v</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/images/copy-vs-move-vector.png" alt="copy-vs-move-vector" /></p>

<p>Pero es que además hay algo aún mejor: todos los contenedores de C++11 han sido optimizados para sacar partido de la semántica de movimiento, por lo que solamente con actualizar a C++ moderno y recompilar es suficiente para aprovecharse de esta nueva optimización allá donde sea posible.</p>

<p>Para terminar esta sección, comentar de pasada que todo esto aplica además al operador de asignación, que desde C++11 tiene una nueva sobrecarga para aceptar referencias a rvalues:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">T</span><span class="o">&amp;</span> <span class="n">T</span><span class="o">::</span><span class="k">operator</span><span class="o">=</span><span class="p">(</span><span class="n">T</span><span class="o">&amp;&amp;</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<h2 id="no-es-oro-todo-lo-que-reluce">No es oro todo lo que reluce…</h2>

<p>…ni más rápido todo lo que pasa por <code class="language-plaintext highlighter-rouge">std::move</code>; y es que esta función realmente <em>no mueve nada</em> (S. Mayers). En cambio, solamente indica que se puede usar la semántica de movimiento, pero si dicha semántica no está implementada, o no puede sacar partido de las condiciones que rodean a ese rvalue, pues no obtendremos ventaja alguna.</p>

<p>Vimos antes que uno de los grandes beneficiados de la semántica de movimiento es la inicialización (o asignación) de contenedores a partir de referencias a rvalues, ya que podían sustituir una nueva reserva de memoria y la consiguiente copia (lineal), por un simple intercambio de valores.</p>

<p>De hecho, y esta es una pregunta que suelo realizar a muchos candidatos, si tuviésemos una estructura con 400 <em>floats</em> y añadiésemos un constructor de movimiento como el anterior, primero, no estaríamos mejorando nada, y segundo, ¡lo estaríamos incluso empeorando!: un constructor de copia realizaría 400 asignaciones, pero el de movimiento… ¡haría 1.200 (3 por cada <code class="language-plaintext highlighter-rouge">swap</code>)!</p>

<p>La semántica de movimiento sólo ayuda cuando somos capaces de ahorrar trabajo basándonos en el hecho de que el argumento va a ser destruido en cuanto acabe la operación. Si esto no nos aporta ninguna ventaja, entonces no ganamos nada.</p>

<h3 id="regla-general">Regla general</h3>

<p>El movimiento de tipos básicos o de composiciones de los mismos no aporta ninguna ventaja frente a la copia.</p>

<p>Ahora bien, la presencia de punteros (incluyendo punteros inteligentes), es un claro indicador de que podríamos mejorar el rendimiento mediante la semántica de movimiento, si bien no reduciendo la complejidad algorítmica del mismo (como con los contenedores), al menos evitando las llamadas al sistema para reservar recursos.</p>

<h2 id="otros-usos-de-la-semántica-de-movimiento">Otros usos de la semántica de movimiento</h2>

<p>Además de permitir optimizaciones, la semántica de movimiento juega un papel muy importante en la definición de tipos de datos <em>no copiables</em>. Pondré tres ejemplos tomados de C++11: <code class="language-plaintext highlighter-rouge">std::thread</code>, <code class="language-plaintext highlighter-rouge">std::mutex</code> y <code class="language-plaintext highlighter-rouge">std::unique_ptr</code>. Dado el objetivo de cada una de estas clases, la copia no tiene ningún sentido y, por ende, no debe estar permitida. ¿Qué es copiar un hilo: arrancar uno nuevo, copiar el estado actual? ¿Tiene sentido copiar un mutex que está garantizando un acceso exclusivo a un recurso? ¿No es contraditorio permit tener más de una copia de un objeto <em>puntero único</em>?</p>

<p>Por otro lado, debemos tener alguna forma en la que dichos objetos puedan ser trasladados de un lugar a otro (por ejemplo, como retorno de una función). Es acá donde la semántica de movimiento entra en juego proporcionando las condiciones para garantizar que los datos de estos objetos no se copian sino que se <em>mueven</em> de un objeto a otro.</p>

<h2 id="copy-elision">Copy elision</h2>

<p>No tiene una relación directa con la semántica de movimiento, pero se confunde con ésta alguna veces. El <em>copy elision</em> es una optimización que permite construir un objeto directamente en la dirección de memoria final de una expresión, omitiendo los constructores de copia intermedios. Por ejemplo, en:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">T</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="n">T</span><span class="p">{};</span> <span class="p">}</span>

<span class="n">T</span> <span class="n">bar</span> <span class="o">=</span> <span class="n">T</span><span class="p">{</span><span class="n">T</span><span class="p">{</span><span class="n">T</span><span class="p">{</span><span class="n">foo</span><span class="p">()}}};</span>
</code></pre></div></div>

<p>sólo se llamaría una vez al constructor por defecto, y directamente sobre la dirección de memoria de <code class="language-plaintext highlighter-rouge">bar</code>, en lugar de la cadena de constructores de copia (o movimiento) y destructores.</p>

<p>Es una optimización muy usada y, de hecho, es la única que viola la regla de <em>as-if</em> (se aplica la optimización aunque el constructor de copia o movimiento que se omiten tiene efectos secundarios).</p>

<p>Existen otras variantes, el RVO (<em>Return Value Optimization</em>) y NRVO (<em>Named Return Value Optimization</em>). La primera está garantizada (si se dan las condiciones el compilador no la puede obviar) desde C++17. Para más información sugiero consultar <a href="https://en.cppreference.com/w/cpp/language/copy_elision">cppreference</a> y <a href="https://stackoverflow.com/q/12953127/1485885">algún hilo en Stack Overflow</a>.</p>

<h2 id="conclusiones">Conclusiones</h2>

<p>La introducción de las referencias a rvalues es una de las principales mejoras introducidas en C++11 ya que asienta las bases para un nuevo tipo de optimizaciones de gran calado, así como la introducción de tipos de datos no-copiables fundamentales.</p>

<p>En este artículo hemos repasado brevemente su sintaxis y su impacto en el código, así como señalado las situaciones en las cuales no aporta mejora alguna, y en qué lo diferencia de algunas optimizaciones del compilador.</p>]]></content><author><name>Carlos Buchart</name></author><category term="c++" /><category term="c++11" /><category term="move-semantics" /><summary type="html"><![CDATA[Abordamos la semántica de movimiento introducida en C++11, los beneficios que aporta a nuestro código, y destruimos algunos mitos y malentendidos.]]></summary></entry></feed>