Jekyll2024-02-08T11:07:15+00:00https://headerfiles.com/feed.xmlHeader FilesBlog personal de Carlos Buchart con artículos sobre C++, Qt, buenas prácticas de programación y expresividad, desarrollo en general y más.Carlos BuchartCómo llamar a una función una única vez2023-10-13T08:00:00+00:002023-10-13T08:00:00+00:00https://headerfiles.com/2023/10/23/ejecutar-una-vez<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="nf">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 <mutex>
</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">&</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="nf">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"><<</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"><<</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"><<</span> <span class="s">"Exception"</span> <span class="o"><<</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"><<</span> <span class="n">a</span> <span class="o"><<</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>Carlos BuchartEstudiamos varias técnicas para restringir, de forma elegante, la ejecución de una función a una única vez.Complejidad algorítmica (parte I)2023-07-17T07:00:00+00:002023-07-17T07:00:00+00:00https://headerfiles.com/2023/07/17/complejidad-algoritmica-1<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 < 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>Carlos BuchartIntroducción al concepto de complejidad algorítmica, su impacto en el desarrollo y algunas consideraciones inicialesRefactoring guiado por constantes en C++2023-06-30T15:00:00+00:002023-06-30T15:00:00+00:00https://headerfiles.com/2023/06/30/refactoring-guiado-por-constantes<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">&</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="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">&</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">&</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">&</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="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">&</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>Carlos BuchartContinuamos estudiando el uso de constantes y explicamos un refactoring muy sencillo que podemos detectar gracias a ellasQue conste porqué construyo con constantes2023-03-27T07:00:00+00:002023-03-27T07:00:00+00:00https://headerfiles.com/2023/03/27/que-conste-porque-construyo-con-constantes<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">&</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"><</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">&&</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">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">&&</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">&&</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">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">&&</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="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="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& 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"><</span><span class="k">class</span> <span class="nc">T</span><span class="p">></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"><</span><span class="n">T</span><span class="o">></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">&</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>Carlos BuchartEnumeramos y razonamos los motivos que me llevan a usar el modificador 'const' en cada momento que puedoCómo cambiar una bombilla2023-02-21T07:00:00+00:002023-02-21T07:00:00+00:00https://headerfiles.com/2023/02/21/how-to-change-a-light-bulb<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>Carlos Buchart¿Qué echo en falta en muchos cursos de programación y no cambia nada incluso con los últimos progresos de la IA?Me gusta el mueve mueve2023-01-29T22:00:00+00:002023-01-29T22:00:00+00:00https://headerfiles.com/2023/01/29/i-like-to-move-it<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&</code>) y referencias constantes a lvalue (<code class="language-plaintext highlighter-rouge">const T&</code>, o <code class="language-plaintext highlighter-rouge">T const&</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&&</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&</code>), en C++11 se introduce el constructor de movimiento, que recibe una referencia a rvalue (<code class="language-plaintext highlighter-rouge">T&&</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"><</span><span class="k">class</span> <span class="nc">T</span><span class="p">></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">&</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">></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"><</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"><</span><span class="k">class</span> <span class="nc">T</span><span class="p">></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">&&</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="nf">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"><</span><span class="kt">int</span><span class="o">></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="nf">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"><</span><span class="kt">int</span><span class="o">></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">&</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">&&</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>Carlos BuchartAbordamos la semántica de movimiento introducida en C++11, los beneficios que aporta a nuestro código, y destruimos algunos mitos y malentendidos.Revisión de código2022-12-16T15:00:00+00:002022-12-16T15:00:00+00:00https://headerfiles.com/2022/12/16/code-review<p>En los últimos años he tenido la oportunidad de trabajar con grandes profesionales del desarrollo de software, y de todos ellos he aprendido muchísimo. Asímismo, en las empresas donde he trabajado he podido comprobar cómo ese conocimiento se transfiere de forma natural de un miembro del equipo a otro, día a día, logrando una verdadera simbiosis.</p>
<p>Esta transferencia tiene lugar de muchas formas, desde charlas formales sobre un tema dado y discusiones acerca de un proyecto o problema puntual, hasta anécdotas contadas durante un café o una cerveza. Además, muchas veeces ocurría de forma indirecta, o inclusivo podríamos decir que <em>pasiva</em>, durante los procesos de revisión de código.</p>
<h2 id="revisión-de-código">Revisión de código</h2>
<p>La revisión de código, para aquellos que no la conozcan, consiste en una actividad en la que otros miembros del equipo ven, estudian, evalúan, critican y proponen mejoras sobre la tarea que tenemos en ese momento entre manos.</p>
<p>Esto se puede hacer de muchas formas, por ejemplo, solicitando directamente a un colega su opinión acerca de una determinada solución; pero la más común es mediante comentarios sobre los cambios en una <em>pull request</em> (o <em>merge request</em>, dependiendo de la plataforma).</p>
<p><img src="/assets/images/code-review-comment.png" alt="Comment in code review" /></p>
<p>Así, durante la revisión, otros miembros del equipo tienen la oportunidad de conocer, cuestionar y proponer mejoras a nuestro código antes de que éste sea integrado (se entiende con estas palabras que nuestros cambios está en una rama y aún no se ha hecho un <em>merge</em> a la rama de desarrollo).</p>
<p>Algunos equipos llevan este proceso un paso más allá y requieren de una aprobación explícita antes de poder incluir los cambios hechos en la rama destino (<em>develop</em>, <em>master</em>…). De esta forma se garantiza que el código ha sido revisado antes de completarse la tarea. Se puede definir que se requiera un mínimo de aprobaciones (por ejemplo 2), y además se puede definir quién puede dar esa aprobación. Así por ejemplo, en ramas normales la aprobación podría ser dada por cualquier miembro del equipo, mientras que la integración con <em>master</em> u otras ramas de producción requerirían la aprobación de los responsables del producto.</p>
<p>Pero, ¿en qué consiste exactamente una revisión de código? Durante una retrospectiva, hace unos meses atrás, salió este tema y, después de hablarlo por un rato, llegué a la conclusión que podríamos dividir las revisiones de código en 3 niveles: rápida (o general), detallada, y en profundidad; o sencillamente, como las solíamos llamar: de nivel 1, 2 y 3. Esta clasificación nos ayudó mucho a centrar los esfuerzos de revisión, pudiendo exprimir al máximo esta gran herramienta.</p>
<h3 id="nivel-1-revisión-rápida-o-general">Nivel 1: revisión rápida o general</h3>
<p>En este nivel el revisor mira el código como un conjunto de líneas casi independientes entre sí: no revisa la tarea como tal sino aspectos genéricos, entre ellos:</p>
<ul>
<li>Conformidad con la guía de estilo</li>
<li>Buenas prácticas de programación para el lenguaje utilizado</li>
<li>Detección de funciones sin ningún <em>test</em> asociado</li>
<li>Falta de documentación</li>
<li>Errores en la documentación, en traducciones, fallos en los recursos</li>
</ul>
<p>De cara a la guía de estilo de código, si bien no es algo obligatorio, y en muchas empresas no la hay, también es cierto que permite centrar la atención en lo importante en lugar de perderlo pensando en cómo indentar una función. Además, si todo el código tiene el mismo estilo, el paso de varios programadores por el mismo no se notará y reducirá el número de cambios entre <em>commits</em> a lo escencial.</p>
<p>Por otro lado, detectar que se han introducido nuevas funciones sin sus correspondientes <em>tests</em> nos ayuda a aumentar la cobertura del mismo de forma natural y por anticipado. Y si lo que se ve es que se ha modificado el comportamiento del código sin tener que actualizar las pruebas existentes, nos da una clara señal de que dichas pruebas no eran tan buenas como creíamos y que deberíamos dedicarles un tiempo a revisarlas.</p>
<p>Pero lo más importante de estas revisiones es que pueden ser hechas por cualquier miembro del equipo ya que no requieren de un especial entendimiento ni de la tarea ni de la solución. Es particularmente útil para los <em>juniors</em> (ayudándoles a ver código más maduro), como a nuevas incorporaciones (adquiriendo familiaridad con el proyecto y las tareas); y dado que pueden hacerse sólo sobre una parte del código, es posible realizarla en cualquier momento libre, o incluso para despejar la mente de otra tarea.</p>
<p>Además de la importante ganancia que tiene para un desarrollador que cualquier miembro del equipo (o de otro equipo incluso) pueda mejorar su código, está el hecho de que los revisores se <em>empapan</em> del trabajo de sus compañeros, tanto de la tarea que se etá llevando a cabo, como del aprendizaje que puedan sacar de ver código ajeno.</p>
<p>Hay que tener en cuenta que este nivel de revisión es suceptible de ser automatizado en gran medida mediante analizadores estáticos, formateadores de código (<code class="language-plaintext highlighter-rouge">clang-format</code> ejecutado durante el pre-commit, por ejemplo), herramientas de <em>coverage</em> automático, etc. Estas automatizaciones no eliminan por inutilizan por completo este nivel de revisión, sino que permiten dedicar el tiempo a otro tipo de comentarios (por ejemplo, decidir si la documentación actual es entendible o si ha quedado desactualizada).</p>
<h3 id="nivel-2-revisión-detallada">Nivel 2: revisión detallada</h3>
<p>Acá ya se requiere un nivel de lectura más detallado, buscando entender mejor los cambios propuestos y lo que de ellos se deriva:</p>
<ul>
<li>Efectos secundarios</li>
<li>Posibles interacciones con otros componentes</li>
<li>Cobertura</li>
<li>Relación con otras tareas (pasadas, en curso, o planificadas)</li>
<li>Propuestas de mejora (optimizaciones, <em>refactorings</em>)</li>
</ul>
<p>Se busca entender si los cambios aplicados pueden generar efectos en otras partes del código o alterar comportamientos existentes. Ejemplos: cambios de un API, nuevos valores por defecto, comportamientos ocultos, código no documentado con soluciones <em>hackeos</em> históricos, etc. Sería recomendable revisar la cobertura de código en caso de que se encuentren efectos secundarios o cambios indirectos.</p>
<p>Se puede analizar el impacto en otros componentes, por ejemplo, proponiendo un <em>refactoring</em> para evitar la duplicidad de código o exponer funcionalidades útiles. Asímismo, esta labor puede extenderse a traer experiencia de tareas pasadas, buscar coordinación o ayuda con tareas en curso, o definir mejor tareas futuras.</p>
<p>Debido al mejor entendimiento del código es posible para los revisores proponer optimizaciones que generen un impacto positivo (se entiende acá además de que se puede reportar cualquier presunta degradación del rendimiento).</p>
<p>Es un buen momento además, aprovechando la dedicación de tiempo, para realizar una prueba de cobertura más a fondo (en el caso de que no esté automatizada).</p>
<p>Puede verse que este nivel requiere de una dedicación mayor que el nivel 1 y un mejor entendimiento tanto de los cambios como del código en general. Si bien todavía podríamos decir que cualquiera puede hacerlas, estas revisiones suelen ser realizadas más por miembros <em>senior</em> del equipo así como afines a la tarea.</p>
<h3 id="nivel-3-revisión-en-profundidad">Nivel 3: revisión en profundidad</h3>
<p>Este último nivel suele estar reservado a personas afines a la tarea y a arquitectos de software, ya que requiere un fuerte conocimiento tanto del trabajo que ha de realizarse como del producto en general. En este nivel es más difícil definir una lista de comentarios posibles, ya que dependen de cada tarea, pero sí podemos resumir los objetivos que persiguen:</p>
<ul>
<li>Validación de la solución</li>
<li>Discusión a fondo de la misma</li>
<li>Preparación para producción</li>
</ul>
<p>Más allá de la implementación detallada, se ha de revisar que la tarea se resuelva por completo (de nada sirve un código maravilloso si no soluciona el problema que debe). Esto implica haber analizado el problema (requerimientos, posibles implementaciones, causas del error, etc.), así como su validación por parte del equipo de QA. Bien podría decirse que la primera parte debe formar parte más del <em>definition of ready</em> que de la revisión de código, pero es importante que esté hecha y entenderla para poder analizar la solución propuesta. Del mismo modo la validación es clave para saber que <em>la teoría se ha llevado a la práctica</em>, por lo que la cobertura de los <em>tests</em> unitarios debe ser adecuada y considerar todos los casos borde posibles.</p>
<p>En este nivel se pueden sugerir mejoras globales de la arquitectura, optimizaciones más agresivas, modificaciones en los procesos de validación para mejorar la cobertura funcional, así como posibles tareas relacionadas pero que se salen del ámbito del problema actual.</p>
<p>Asímismo, hay que mantener la atención en que la solución debe ser <em>production ready</em> (salvo el caso de pruebas de concepto o tareas parciales). Esto incluye verificar que todos los aspectos que rodean al cambio, tales como traducciones, instalación de dependencias, <em>feature flags</em>, mecanismos de despliegues a tener en cuenta, notificación de cambio de APIs, entre otros, hayan sido tenidos en cuenta (obviamente, si existe una tarea diferente para ello se ha de relegar a la misma).</p>
<h2 id="consideraciones-finales">Consideraciones finales</h2>
<p>La revisión de código es una herramienta técnica que atañe principalmente a los implicados en la ejecución de la tarea (desarrolladores principalmente, aunque podríamos considerar a DevOps y QAs si el código está relacionado con dichas áreas). No tiene mucho sentido que los <em>product owners</em> o <em>managers</em> se paseen por las revisiones de código de normal: para saber lo que hace el equipo se disponen de otras herramientas, tales como las <em>Scrum dailies</em>.</p>
<p>Por otro lado, si bien la implicación de un QA en la revisión de código genérico no es obligatoria, personalmente siempre he obtenido mejores resultados cuando están en contacto cercano con la tarea. En algunos casos se puede definir una tarea de validación explícita antes de dar por bueno el desarrollo, que podría implicar, <em>per se</em>, el desarrollo de nuevas pruebas automatizadas, <em>tests</em> de regresión, etc. En otros casos el <em>ticket</em> se reenvará a los equipos de validación y pruebas para su consideración para el siguiente lanzamiento.</p>
<p>Para finalizar, es importante hablar acerca de los modales: la revisión de código es una parte de nuestro trabajo, y debe realizarse con la misma profesionalidad y respeto hacia nuestros colegas. Así, si hay que decir que un cambio no es correcto o incluso dañino, se dice, pero con respeto y amabilidad. De la misma forma también se puede aprovechar para valorar positivamente un buen trabajo. De cara a recibir comentarios, recordad que el objetivo de los comentarios no es el autor sino la mejora del código, del producto y de la empresa; por lo que hay que tomarlos de forma constructiva. En lo personal, creo que he aprendido tanto durante las revisiones de código como de Stack Overflow 😉.</p>
<h2 id="conclusiones">Conclusiones</h2>
<p>Hemos visto una breve introducción a las revisiones de código y su importancia, así como un breve esquema de los diferentes tipos de revisiones que podemos hacer para sacarles el mayor beneficio posible.</p>Carlos BuchartReflexiones sobre los procesos de revisión de código y su impacto en la calidad del software y del equipo.Resolviendo warnings con strict casting2022-09-17T22:22:00+00:002022-09-17T22:22:00+00:00https://headerfiles.com/2022/09/18/strict-cast<p>Es bien sabido que, en términos generales, los <em>warnings</em> del compilador son más que mensajes de un <em>puritano del lenguaje</em>; casi siempre son una señal de que algo no está del todo bien y que deberíamos revisar: asignaciones en lugar de comparaciones, valores de un <em>enum</em> que no se han tomado en cuenta en un <em>switch</em>, variables sin utilizar (si hay muchas para una misma función puede ser una señal de que necesitamos un <em>refactoring</em>), funciones que no devuelven valor cuando su declaración dice que sí, uso de funciones inseguras, etc.</p>
<p>Uno de los <em>warnings</em> que seguramente más hayamos visto es el de conversión de un tipo más <em>grande</em> a uno más <em>chico</em> (o entre enteros con y sin signo), con la posible pérdida de precisión o valores inesperados.</p>
<p>Esto suele darse muy especialmente cuando pasamos un valor entre dos módulos que fueron diseñados con requerimientos diferentes y ahora tienen la mala suerte de vivir juntos. Algunas veces no pasará nada y será seguro su uso; en otros tendremos que recurrir a una función de conversión, o refactorizar uno de los módulos para ajustarnos a esta nueva comunicación.</p>
<p>En los casos en los que la conversión se considere segura probablemente querramos deshacernos del mensaje: bien sea por seguir una regla del equipo de no tener <em>warnings</em>, bien para poder seguir la compilación en caso de que se traten como errores, o por simple manía de no querer que el compilador nos <em>contamine</em> frente a otros mensajes más relevantes. En cualquier caso esto se puede hacer mediante un <code class="language-plaintext highlighter-rouge">static_cast</code>, que además nos asegurará en tiempo de compilación que los tipos son “compatibles” entre sí, y pongo las comillas porque esto tiene una coletilla que veremos más adelante.</p>
<p>Antes de proseguir, comentar que todos los ejemplos serán compilados teniendo habilitados los <em>warnings</em> de conversión entre tipos:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>g++ <span class="nt">-std</span><span class="o">=</span>c++20 <span class="nt">-Wconversion</span> <span class="nt">-Wsign-conversion</span> <span class="nt">-Wall</span> main.cpp
</code></pre></div></div>
<h2 id="caso-de-estudio">Caso de estudio</h2>
<p>Supongamos pues el caso de que necesitemos unir dos módulos: el motor físico de un simulador de conducción y el controlador de actuadores de la cabina de entrenamiento. El primero debe pasarle al segundo la velocidad del vehículo. Ambos módulos fueron diseñados por separado y ahora nos toca integrarlos.</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <iostream>
</span>
<span class="k">using</span> <span class="k">namespace</span> <span class="n">std</span><span class="p">;</span>
<span class="kt">int16_t</span> <span class="nf">get_speed</span><span class="p">(</span><span class="kt">int16_t</span> <span class="n">time</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">return</span> <span class="n">time</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">write_to_register</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">reg</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">value</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Write "</span> <span class="o"><<</span> <span class="n">value</span> <span class="o"><<</span> <span class="s">" to 0x"</span> <span class="o"><<</span> <span class="n">uppercase</span> <span class="o"><<</span> <span class="n">hex</span> <span class="o"><<</span> <span class="n">reg</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">auto</span> <span class="k">const</span> <span class="n">speed</span> <span class="o">=</span> <span class="n">get_speed</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Speed factor: "</span> <span class="o"><<</span> <span class="n">speed</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="n">write_to_register</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="n">speed</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>
<h3 id="problema-y-solución-inicial">Problema y solución inicial</h3>
<p>Todo <em>marcha sobre ruedas</em> hasta que vemos un <em>warning</em> que el actuador usa registros de 16 bits <em>sin signo</em>, mientras que la velocidad del simulador se devuelve como un entero de 16 bits <em>con signo</em> (negativo indica retroceso).</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>main.cpp: In <span class="k">function</span> <span class="s1">'int main()'</span>:
main.cpp:26:29: warning: conversion to <span class="s1">'uint16_t'</span> <span class="o">{</span>aka <span class="s1">'short unsigned int'</span><span class="o">}</span> from <span class="s1">'short int'</span> may change the sign of the result <span class="o">[</span><span class="nt">-Wsign-conversion</span><span class="o">]</span>
26 | write_to_register<span class="o">(</span>0xFF, speed<span class="o">)</span><span class="p">;</span>
| ^~~~~
</code></pre></div></div>
<p>Consultando el manual vemos que no es un problema del hardware sino del API del controlador (el hardware <em>considera</em> los valores desde el 32.768 hasta el 65.535 como negativos en complemento a 2, es decir, con signo, sólo que la API fue mal escrita).</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Speed <span class="nb">factor</span>: <span class="nt">-1</span>
Write 65535 to 0xFF
</code></pre></div></div>
<p>Pasado este susto decidimos silenciar el <em>warning</em> con un <code class="language-plaintext highlighter-rouge">static_cast</code>:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">auto</span> <span class="k">const</span> <span class="n">speed</span> <span class="o">=</span> <span class="n">get_speed</span><span class="p">(</span><span class="o">-</span><span class="mi">3</span><span class="p">);</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Speed factor: "</span> <span class="o"><<</span> <span class="n">speed</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="n">write_to_register</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="k">static_cast</span><span class="o"><</span><span class="kt">uint16_t</span><span class="o">></span><span class="p">(</span><span class="n">speed</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>Como nota adicional, y a efectos de facilitar el entendimiento de lo que sucede, añadiremos un mensaje adicional para mostrar el valor con signo correspondiente:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">write_to_register</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">reg</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">value</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">auto</span> <span class="k">const</span> <span class="n">signed_value</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">make_signed_t</span><span class="o"><</span><span class="k">decltype</span><span class="p">(</span><span class="n">value</span><span class="p">)</span><span class="o">>></span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Write "</span> <span class="o"><<</span> <span class="n">value</span> <span class="o"><<</span> <span class="s">" to 0x"</span> <span class="o"><<</span> <span class="n">uppercase</span> <span class="o"><<</span> <span class="n">hex</span> <span class="o"><<</span> <span class="n">reg</span> <span class="o"><<</span> <span class="n">dec</span> <span class="o"><<</span>
<span class="s">". Signed value: "</span> <span class="o"><<</span> <span class="n">signed_value</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Podemos ejecutar este ejemplo inicial en <a href="https://coliru.stacked-crooked.com/a/f92216805b2a4a9c">Coliru</a>.</p>
<h2 id="primer-problema-cambios-en-la-api-emisora-valor-de-retorno">Primer problema: cambios en la API emisora (valor de retorno)</h2>
<p>Como ejercicio, supongamos que el equipo de diseño del motor físico ha aumentado la potencia del sistema y ahora es capaz de reportar un mayor rango de velocidad, pasando de 16 bits a 32:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int32_t</span> <span class="nf">get_speed</span><span class="p">(</span><span class="kt">int32_t</span> <span class="n">time</span><span class="p">);</span>
<span class="c1">// ...</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">auto</span> <span class="k">const</span> <span class="n">speed</span> <span class="o">=</span> <span class="n">get_speed</span><span class="p">(</span><span class="o">-</span><span class="mi">128000</span><span class="p">);</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Speed: "</span> <span class="o"><<</span> <span class="n">speed</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="n">write_to_register</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="k">static_cast</span><span class="o"><</span><span class="kt">uint16_t</span><span class="o">></span><span class="p">(</span><span class="n">speed</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>Cuando ejecutamos el sistema todo va bien, pero ya en producción algunos clientes reportan un comportamiento errático cuando el sistema alcanza grandes velocidades: ¡de repente el vehículo se ralentiza en lugar de acelerar!</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Speed: <span class="nt">-128000</span>
Write 3072 to 0xFF. Signed value: 3072
</code></pre></div></div>
<p>Como podemos imaginar, el problema reside en que el <code class="language-plaintext highlighter-rouge">static_cast<uint16_t></code> está ocultado un <em>warning</em> que, de estar activo, nos habría alertado del <em>downcastings</em> de 32 a 16 bits. El escenario completo se puede ver <a href="https://coliru.stacked-crooked.com/a/5828f9dfc9738dfd">acá</a>.</p>
<h3 id="solución-propuesta-strict_cast">Solución propuesta: <code class="language-plaintext highlighter-rouge">strict_cast</code></h3>
<p>Tenemos entonces dos problemas en simultáneo: silenciar el <em>warning</em> pero recuperándolo cuando haya cambiado el escenario en el que fue silenciado. Desafortunadamente esto no es posible con ninguno de los operadores de <code class="language-plaintext highlighter-rouge">casting</code> estándar de C++, así que presentaremos uno que nos permite todo esto. Por iniciativa propia he decidido nombrar a esta solución <code class="language-plaintext highlighter-rouge">strict_cast</code>, y se puede definir como</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o"><</span><span class="k">typename</span> <span class="nc">ExpectedFrom</span><span class="p">,</span> <span class="k">typename</span> <span class="nc">To</span><span class="p">,</span> <span class="k">typename</span> <span class="nc">From</span><span class="p">></span>
<span class="k">constexpr</span> <span class="n">To</span> <span class="nf">strict_cast</span><span class="p">(</span><span class="n">From</span><span class="o">&&</span> <span class="n">from</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">static_assert</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">is_same_v</span><span class="o"><</span><span class="n">ExpectedFrom</span><span class="p">,</span> <span class="n">From</span><span class="o">></span><span class="p">,</span> <span class="s">"Invalid expected type"</span><span class="p">);</span>
<span class="k">return</span> <span class="k">static_cast</span><span class="o"><</span><span class="n">To</span><span class="o">></span><span class="p">(</span><span class="n">from</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Para los más curiosos, acá no hay riesgo de deducción de tipos ya que, aunque se puede deducir el argumento no se puede deducir el tipo de retorno, por lo que hay que indicarlo explícitamente y, como es el segundo argumento del <em>template</em>, nos obliga entonces a indicar también el tipo esperado. El último tipo sí lo deducimos automáticamente para asegurar que siempre tenemos el tipo original.</p>
<p>Además, podemos notar cómo hemos forzado los errores mediante el <code class="language-plaintext highlighter-rouge">static_assert</code>. Así, si estamos usando este operador podemos desentendernos de la configuración del compilador y de <em>warnings</em> ignorados.</p>
<p>Incorporando esta solución a nuestro ejemplo anterior (la versión <code class="language-plaintext highlighter-rouge">int32_t</code>), tenemos:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <iostream>
</span>
<span class="k">using</span> <span class="k">namespace</span> <span class="n">std</span><span class="p">;</span>
<span class="kt">int32_t</span> <span class="nf">get_speed</span><span class="p">(</span><span class="kt">int16_t</span> <span class="n">speed</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">return</span> <span class="n">speed</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">write_to_register</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">reg</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">value</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">auto</span> <span class="k">const</span> <span class="n">signed_value</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">make_signed_t</span><span class="o"><</span><span class="k">decltype</span><span class="p">(</span><span class="n">value</span><span class="p">)</span><span class="o">>></span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Write "</span> <span class="o"><<</span> <span class="n">value</span> <span class="o"><<</span> <span class="s">" to 0x"</span> <span class="o"><<</span> <span class="n">uppercase</span> <span class="o"><<</span> <span class="n">hex</span> <span class="o"><<</span> <span class="n">reg</span> <span class="o"><<</span> <span class="n">dec</span> <span class="o"><<</span>
<span class="s">". Signed value: "</span> <span class="o"><<</span> <span class="n">signed_value</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">template</span><span class="o"><</span><span class="k">typename</span> <span class="nc">ExpectedFrom</span><span class="p">,</span> <span class="k">typename</span> <span class="nc">To</span><span class="p">,</span> <span class="k">typename</span> <span class="nc">From</span><span class="p">></span>
<span class="k">constexpr</span> <span class="n">To</span> <span class="nf">strict_cast</span><span class="p">(</span><span class="n">From</span> <span class="k">const</span><span class="o">&</span> <span class="n">from</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">static_assert</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">is_same_v</span><span class="o"><</span><span class="n">ExpectedFrom</span><span class="p">,</span> <span class="n">From</span><span class="o">></span><span class="p">,</span> <span class="s">"Invalid expected type"</span><span class="p">);</span>
<span class="k">return</span> <span class="k">static_cast</span><span class="o"><</span><span class="n">To</span><span class="o">></span><span class="p">(</span><span class="n">from</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">auto</span> <span class="k">const</span> <span class="n">speed</span> <span class="o">=</span> <span class="n">get_speed</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="n">cout</span> <span class="o"><<</span> <span class="s">"Speed: "</span> <span class="o"><<</span> <span class="n">speed</span> <span class="o"><<</span> <span class="n">endl</span><span class="p">;</span>
<span class="n">write_to_register</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="n">speed</span><span class="p">);</span> <span class="c1">// <-- warning here</span>
<span class="n">write_to_register</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="k">static_cast</span><span class="o"><</span><span class="kt">uint16_t</span><span class="o">></span><span class="p">(</span><span class="n">speed</span><span class="p">));</span> <span class="c1">// <-- no warning here</span>
<span class="n">write_to_register</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="n">strict_cast</span><span class="o"><</span><span class="kt">int16_t</span><span class="p">,</span> <span class="kt">uint16_t</span><span class="o">></span><span class="p">(</span><span class="n">speed</span><span class="p">));</span> <span class="c1">// <-- error here</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 código completo se puede ver, como antes, en <a href="https://coliru.stacked-crooked.com/a/7994971a47b1ee59">Coliru</a>.</p>
<h2 id="segundo-problema-cambios-en-la-api-receptora-argumentos">Segundo problema: cambios en la API receptora (argumentos)</h2>
<p>El operador propuesto funciona únicamente con los tipos conocidos <em>antes</em> de ejecutarse el operador (el tipo de retorno esperado y el tipo de retorno real), pero no puede hacer nada con el tipo real del argumento en el que se usará el resultado, por lo que todavía quedan casos en los cuales podemos tener un error.</p>
<p>Para ilustrarlo digamos que, pasado un tiempo, nos anuncian que se cambiará el controlador de los actuadores por uno más moderno de 32 bits: nos dan acceso a la nueva API, todo compila sin problemas y se pasan los <em>tests</em>, pero poco después las pruebas de integración revelan un fallo: el coche no es capaz de retroceder, en su lugar acelera a tope y por fuera de los límites físicos de los actuadores.</p>
<p>Rápidamente pensamos en un problema por el cambio de plataforma y poco después encontramos que, efectivamente, la función de escritura al hardware cambió a:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">write_to_register</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">reg</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">value</span><span class="p">);</span>
</code></pre></div></div>
<p>El <em>casting</em> (incluso nuestro ya amado <code class="language-plaintext highlighter-rouge">strict_cast</code>) pasó a escribir siempre valores en el rango de velocidades positivas para 32 bits; y claro, como -1 con signo es 65535 sin signo, pues el sistema se salía de rango a la mínima.</p>
<p>Acá la cosa se complica porque la conversión es válida y el error viene del <em>doble casting</em> que hemos aplicado (el explícito del <code class="language-plaintext highlighter-rouge">strict_cast</code> y el implícito de 16 a 32 bits). Aún así, tenemos una forma de detectarlo pero su uso es menos intuitivo.</p>
<h3 id="solución-propuesta-strict_args">Solución propuesta: <code class="language-plaintext highlighter-rouge">strict_args</code></h3>
<p>Lo primero que necesitamos es poder extraer el tipo de los argumentos de una función. Para ello construiremos un <em>invocador</em> que recibirá la función que queremos llamar y sus argumentos. Luego usaremos una función <em>template</em> que nos devolverá una tupla con los argumentos de la función en cuestión (créditos a <a href="https://stackoverflow.com/a/18872019/1485885">Cassio Neri</a>), y la compararemos con una construida en base a los tipos de los valores pasados. Si todo va bien, llamamos a la función:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="nc">R</span><span class="p">,</span> <span class="k">typename</span><span class="o">...</span> <span class="nc">Args</span><span class="p">></span>
<span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o"><</span><span class="n">Args</span><span class="p">...</span><span class="o">></span> <span class="n">extract_args</span><span class="p">(</span><span class="n">R</span><span class="p">(</span><span class="n">Args</span><span class="p">...));</span>
<span class="k">template</span><span class="o"><</span><span class="k">typename</span> <span class="nc">Function</span><span class="p">,</span> <span class="k">typename</span><span class="o">...</span> <span class="nc">ExpectedArgs</span><span class="p">></span>
<span class="k">constexpr</span> <span class="k">auto</span> <span class="nf">strict_args</span><span class="p">(</span><span class="n">Function</span><span class="o">&&</span> <span class="n">f</span><span class="p">,</span> <span class="n">ExpectedArgs</span><span class="p">...</span> <span class="n">args</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">using</span> <span class="n">function_args_t</span> <span class="o">=</span> <span class="k">decltype</span><span class="p">(</span><span class="n">extract_args</span><span class="p">(</span><span class="n">f</span><span class="p">));</span>
<span class="k">using</span> <span class="n">expected_args_t</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o"><</span><span class="n">ExpectedArgs</span><span class="p">...</span><span class="o">></span><span class="p">;</span>
<span class="k">static_assert</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">is_same_v</span><span class="o"><</span><span class="n">function_args_t</span><span class="p">,</span> <span class="n">expected_args_t</span><span class="o">></span><span class="p">,</span> <span class="s">"Invalid expected types"</span><span class="p">);</span>
<span class="k">return</span> <span class="n">f</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">forward</span><span class="o"><</span><span class="n">ExpectedArgs</span><span class="o">></span><span class="p">(</span><span class="n">args</span><span class="p">)...);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Como se podrá ver a continuación, su uso es un poco más <em>artificial</em>, aunque muy explícito. El ejemplo completo en <a href="https://coliru.stacked-crooked.com/a/a8db1f1dce2263fd">Coliru</a>.</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">strict_args</span><span class="p">(</span><span class="n">write_to_register</span><span class="p">,</span> <span class="k">static_cast</span><span class="o"><</span><span class="kt">uint16_t</span><span class="o">></span><span class="p">(</span><span class="mh">0xFF</span><span class="p">),</span> <span class="n">strict_cast</span><span class="o"><</span><span class="kt">int16_t</span><span class="p">,</span> <span class="kt">uint16_t</span><span class="o">></span><span class="p">(</span><span class="n">speed</span><span class="p">));</span>
</code></pre></div></div>
<p>Puede notarse que he tenido que añadir un <code class="language-plaintext highlighter-rouge">strict_cast<uint16_t></code> para el número del registro, que antes no hemos necesitado. Esto se debe a que en los ejemplos anteriores el compilador es lo suficientemente listo como para saber que <code class="language-plaintext highlighter-rouge">0xFF</code> <em>cabe</em> perfectamente dentro de un <code class="language-plaintext highlighter-rouge">uint16_t</code>, mientras que con el <code class="language-plaintext highlighter-rouge">strict_call</code> debe deducir el tipo de <code class="language-plaintext highlighter-rouge">0xFF</code> <em>antes</em> de saber que debe usarlo como 16-bits, por lo que deduce su tipo normal, un <code class="language-plaintext highlighter-rouge">int</code>. Eso sí, como se trata de un literal no me he molestado en usar el <code class="language-plaintext highlighter-rouge">strict_cast</code> en esta ocación ;).</p>
<h2 id="otras-posibles-soluciones">Otras posibles soluciones</h2>
<p>En el caso de que dispongamos de control de la API conflictiva (<code class="language-plaintext highlighter-rouge">get_speed</code> o <code class="language-plaintext highlighter-rouge">write_register</code>), podríamos mejorar la solución aún más sin necesidades de los operadores presentados, mediante el uso de tipos fuertemente tipados (para más información se pueden consultar los artículos sobre <a href="https://headerfiles.com/2021/02/07/expressive-args/">booleanos fuertemente tipados</a> y <a href="https://headerfiles.com/2021/07/06/expressive-args-2/">argumentos fuertemente tipados</a>).</p>
<h2 id="conclusiones">Conclusiones</h2>
<p>Hemos comentado la importancia de prestar atención a los <em>warnings</em> de compilación y de los problemas que nos puede atraer el silenciarlos. Para resolverlo hemos presentado dos operadores: <code class="language-plaintext highlighter-rouge">strict_cast</code> para asegurarnos que el tipo del dato origen coincide con el que esperamos, y <code class="language-plaintext highlighter-rouge">strict_args</code> para comprobar si los tipos de datos de los argumentos han cambiado.</p>
<p><em>Nota final:</em> la solución propuesta es compatible con C++17. Si se quisiese usar en C++14 deberíamos cambiar las líneas del tipo <code class="language-plaintext highlighter-rouge">std::is_same_v<T, U></code> por <code class="language-plaintext highlighter-rouge">std::is_same<T, U>::value</code>.</p>Carlos BuchartAlternativas expresivas al type casting y con detección de cambios de API.Documentar, sí, ¿pero dónde?2022-07-14T22:45:00+00:002022-07-14T22:45:00+00:00https://headerfiles.com/2022/07/15/documentar-si-pero-donde<p>Mis primeras experiencias programando se podrían catalogar formalmente de <em>garabatos</em>: un montón de código que a duras penas hacía lo que yo quería que hiciese (el hecho de que fuese en BASIC no ayudaba mucho, todo hay que decirlo). En ese entonces tampoco disponía de conexión a Internet, y aunque la tuviese, tampoco habría encontrado gran cosa en él (aún).</p>
<p>Al poco tiempo aprendí la importancia de dejar, usando palabras en cristiano, una explicación de aquellas líneas. Y así se inició ese viaje en lograr que el código lo entendiese no sólo el ordenador, sino también otro ser humano (que, como pasa inequívocamente, casi siempre era yo mismo poco tiempo después).</p>
<h2 id="las-etapas-de-la-documentación">Las etapas de la documentación</h2>
<p>Al principio uno ve la documentación como algo tedioso e innecesario: ¿por qué he de poner en la lengua de Cervantes (o Shakespeare) lo que esa hermosa línea de código hace, si se ve a leguas? Bueno, cualquiera que haya vuelto a un código suyo escrito pocas semanas atrás sabrá responder a esta pregunta rápidamente (aunque no todo son comentarios, pero hablaremos de ello en un rato).</p>
<p>Poco después casi siempre uno pasa por un período oscuro, opuesto por completo a la falta de documentación pero igual de malo: la <em>sobredocumentación</em>. Si no poner ningún comentario es malo, parafrasear cada comando, instrucción y ciclo de reloj no solo es una pérdida de tiempo en ese momento, es además una pérdida de tiempo a futuro cuando se esté leyendo el código y una pérdida de tiempo aún mayor ya que hay que mantener una documentación que es tan rígida que con el mínimo cambio queda obsoleta.</p>
<p>En términos generales sabemos bien lo que una línea individual hace: leer un fichero, incrementar un valor, grabar un valor a disco… El problema no es <em>qué</em> hace una línea, sino <em>qué se supone que queremos hacer</em> con el conjunto (bloque, función, clase), el <em>por qué</em> se hace. Para el ejemplo anterior bien podría ser <em>generar y almacenar el siguiente ID único</em>. Esto hace a la documentación más útil y además más duradera en el tiempo, ya que no depende del código sino del diseño de la solución y de los requerimientos.</p>
<h2 id="código-expresivo">Código expresivo</h2>
<p>Cuando llegamos a este punto entendemos que, aunque no haya que documentar cada línea del código, sí que hay que escribir un código que sea legible. No es lo mismo <code class="language-plaintext highlighter-rouge">int ab23 = get_value(42, 3.14, 1984);</code> que</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">constexpr</span> <span class="k">auto</span> <span class="n">ANSWER</span><span class="p">{</span><span class="mi">42</span><span class="p">};</span>
<span class="k">constexpr</span> <span class="k">auto</span> <span class="n">PI</span><span class="p">{</span><span class="mf">3.14</span><span class="p">};</span>
<span class="k">constexpr</span> <span class="k">auto</span> <span class="n">BEST_YEAR</span><span class="p">{</span><span class="mi">1984</span><span class="p">};</span>
<span class="kt">int</span> <span class="n">common_digits_count</span> <span class="o">=</span> <span class="n">get_number_of_common_digits</span><span class="p">(</span><span class="n">ANSWER</span><span class="p">,</span> <span class="n">PI</span><span class="p">,</span> <span class="n">BEST_YEAR</span><span class="p">);</span>
</code></pre></div></div>
<p>Esto no sólo aplica a los nombres de variables, tipos y funciones. La expresividad también está en el correcto uso del lenguaje en el que programamos. Por listar algunos:</p>
<ul>
<li>Uso de la biblioteca estándar (no reinventar la rueda, usando un idioma común a otros programadores). ¿Para qué usar un bucle <code class="language-plaintext highlighter-rouge">for</code> recorriendo todo el vector en busca de un registro específico, si tenemos <code class="language-plaintext highlighter-rouge">std::find_if</code>?</li>
<li>Seguir los <em>guidelines</em> generales del lenguaje: como el lenguaje principal de este blog es C++, acá tenéis los <a href="https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines"><em>guidelines</em> oficiales</a>. Por otro lado, Python por ejemplo usa el <a href="https://peps.python.org/pep-0008/">PEP 8</a>.</li>
<li>Un correcto uso de la semántica propia (¿por qué usar lenguaje imperativo cuando se soporta y prefiere el funcional?)</li>
</ul>
<p>En resumen, el mejor comentario es el que no se necesita, ya que en ese caso el código habla por sí mismo. Esto no quita que debamos indicar el propósito general si éste no se puede extraer fácilmente del propio código. Veamos cómo documentar el resto.</p>
<h2 id="documentación-de-api">Documentación de API</h2>
<p>Esta documentación suele estar en los ficheros públicos del código, aquellos que <em>ven</em> otros programadores, y se necesita para entender cómo usar las interfaces expuestas, sus funciones, parámetros, propósito de las clases, etc.</p>
<p>Además, estos comentarios suelen diferenciarse de los demás en que tienen una sintaxis particular (dependiendo del lenguaje y otras herramientas de documentación). Por ejemplo, si usamos C++ y Doxygen, podríamos ver algo como</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Generate a unique private key for a given table.
* @param table Table for which the key is being generated.
* @return int Unique key.
*/</span>
<span class="kt">int</span> <span class="nf">generate_unique_key</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">&</span> <span class="n">table</span><span class="p">);</span>
</code></pre></div></div>
<p>Es importante destacar que si las APIs están en la frontera de nuestro servicio (por ejemplo, una API REST o un sistema de mensajes), la documentación generada debe estar disponible a otros equipos, tanto de desarrollo como de QA. Esto puede hacerse bien exportando la documentación generada, o bien mediante sistemas de definición de APIs como RAML u OpenAPI que además permitan generar las APIs requeridas por cada proyecto de forma automática a partir de la misma especificación.</p>
<h2 id="documentación-de-lógica">Documentación de lógica</h2>
<p>Con estos comentarios buscamos resumir el algoritmo, el propósito del código. Muchas veces puede ser un breve resumen al comienzo de una función. Otras se pondrá un comentario para describir lo que se busca con un determinado bloque de código, aunque hay que estar atentos a estos casos, ya que podría ser un indicador de que podemos refactorizar y extraer una función.</p>
<h3 id="casos-particulares-y-casos-borde">Casos particulares y casos borde</h3>
<p>Esta documentación es de suma importancia, ya que no se suele poder deducir del código. Son casos especiales para los que el diseño no está preparado, <em>corner cases</em> encontrados en producción o código que simplifica la lógica atajando determinadas situaciones.</p>
<p>En estos casos la documentación busca salvaguardar ese conocimiento de nuestra volátil memoria (o incluso de nuestra volatilidad en la empresa). En el caso de los <em>hotfixes</em>, suele ser buena idea dejar constancia del ID del <em>ticket</em> asociado, de forma que se puede entender mejor el contexto, cómo se produce el error, etc. Comentar por último que estos comentarios son útiles para ayudar a futuros <em>refactorers</em> a entender mejor el problem.</p>
<h2 id="documentación-externa-al-código">Documentación externa al código</h2>
<p>Hay otra parte vital de la documentación y es aquella que describe al conjunto. No siempre podemos entender, o siquiera usar un módulo si no sabemos qué problema resuelve, el diseño de las clases, su interoperabilidad, etc.</p>
<p>En esta parte digamos que siempre hay poco de conflicto sobre cómo documentar: están los que prefieren un fichero <code class="language-plaintext highlighter-rouge">README.md</code> en el proyecto, los que abogan por un directorio completo de documentación, los que prefieren ponerla en un gestor de documentos, en una <em>wiki</em>…</p>
<p>En términos generales empecemos diciendo que lo más importante es que <em>exista</em>. Si no hay documentación de poco sirve enfrascarnos en una discusión de dónde tiene que ir.</p>
<p>Lo siguiente es que debe ser <em>encontrable</em>. Cualquiera que la necesite debería poder buscarla y acceder a ella (roles aparte).</p>
<p>Por último, debe ser <em>usable</em>. Es decir, que nos aporte la información que necesitamos. Esto incluye que esté actualizada (con el código, con los requerimientos), y que sea adecuada (navegación, contenido, nivel de detalle).</p>
<h3 id="documentación-de-especificaciones-funcionales">Documentación de especificaciones funcionales</h3>
<p>Estos documentos nos indican lo que debería hacer nuestra aplicación, servicio, módulo… Normalmente viene dado por el <em>Product Owner</em>, que a su vez lo ha redactado a partir de los requisitos del Negocio. Un ejemplo de ello serían los diagramas de casos de uso.</p>
<p>Por definición, un desarrollador debería ser un <em>lector</em> de este documento, pero no un <em>escritor</em>, no debería modificarlo ya que podría caer en la tentación de ajustar los requerimientos al comportamiento del sistema, y no al contrario que es como debería ser.</p>
<p>Debido a esto, estos documentos deberían estar separados del código y en un lugar visible por todos los equipos involucrados: desarrollo, diseño, validación… Este lugar podría ser desde algo tan completo como un DMS (como Confluence), hasta algo más sencillo como una wiki o una carpeta compartida en Google Drive.</p>
<h3 id="documentación-del-diseño">Documentación del diseño</h3>
<p>Si los documentación de especificaciones eran el <em>qué</em> hay que hacer, el diseño de software viene siendo el <em>cómo</em> está pensada la solución, y puede presentarse en diferentes niveles de abstracción: diagramas de clases, de estados, de secuencia, de colaboración, etc. (Para más información se pueden consultar los distintos <a href="https://es.wikipedia.org/wiki/Lenguaje_unificado_de_modelado">diagramas UML</a>).</p>
<p>Como es evidente, dichos diagramas y demás documentos son útiles sólo si se corresponden con el código, si le representan. Si no más bien crean confusión. Por ejemplo, ¿el diagrama de secuencia es correcto y la implementación es errónea? ¿o más bien el diagrama se quedó obsoleto por no actualizarlo con los cambios en el código?</p>
<p>Dicho esto, lo más natural es versionar esta documentación a la par que el código, posiblemente como parte del mismo repositorio; o generar parte de ella a partir del código (por ejemplo los diagramas de clases o de colaboración).</p>
<h2 id="conclusiones">Conclusiones</h2>
<p>Hemos comentado a lo largo de este artículo la importancia de documentar qué hace nuestro código, cómo lo hace, cómo se comunica, de dejar constancia de la experiencia adquirida. Asimismo hemos presentado una propuesta de distribución de la documentación que la pone cercana a los actores interesados así y que permite mantenerla útil a lo largo del tiempo.</p>
<h2 id="créditos">Créditos</h2>
<ul>
<li>Icono de la imagen de cabecera por <a href="https://www.flaticon.com/free-icons/documents">Freepik - Flaticon</a>.</li>
</ul>Carlos BuchartAlgunas reflexiones sobre la documentación de código.Extendiendo el autocompletado en Bash2022-04-26T06:45:00+00:002022-04-26T06:45:00+00:00https://headerfiles.com/2022/04/26/autocompletado-bash<p>Personalmente creo que hay cuatro acciones de teclado que consumen el 70% de mi tiempo en una terminal: <kbd>Enter</kbd>, <kbd>Control-C</kbd>, <kbd>Arriba</kbd> y <kbd>Tab</kbd>. Las dos primeras para iniciar y parar comandos, y las dos últimas para agilizar la escritura, bien sea buscando en el historial o bien completando el nombre del comando actual.</p>
<p>Respecto al autocompletado, algunas <em>shell</em>, como Bash, permiten además hacer un autocompletado contextual, es decir, que una vez introducido el nombre del comando que se quiere ejecutar el motor de autocompletado ofrecerá las opciones pertinentes en base a dicho comando y a las opciones previamente seleccionadas. Esto no sólo ahorra tiempo de escritura, sino que además sirve de complemento a la ayuda del propio comando.</p>
<p>En esta entrada expondremos cómo crear un <em>script</em> de autocompletado propia, de forma que podamos incluirlo en nuestros proyectos. Aunque me centraré en la <em>shell</em> Bash, esta funcionalidad también es posible para otras: en cmd (Windows) mediante <a href="https://github.com/mridgers/clink">clink</a> (usando Lua), PowerShell mediante funciones TabExpansion, zsh mediante <a href="https://github.com/marlonrichert/zsh-autocomplete">zsh-autocomplete</a> (aunque en este caso habría que extender el proyecto base).</p>
<h2 id="pre-requisitos">Pre-requisitos</h2>
<p>Además de tener Bash como <em>shell</em> activa, necesitaremos el paquete <code class="language-plaintext highlighter-rouge">bash-completion</code> y activarlo en nuestra sesión. Podemos seguir las instrucciones listadas <a href="https://askubuntu.com/a/545578/1057035">acá</a>.</p>
<h2 id="diseñando-el-script">Diseñando el <em>script</em></h2>
<p>Comenzaremos detallando el comando al que queremos dar soporte, para luego mostrar y explicar el <em>script</em> en cuestión.</p>
<h3 id="sintaxis-del-comando">Sintaxis del comando</h3>
<p>Vamos a suponer que nuestro comando se llama <code class="language-plaintext highlighter-rouge">headerfiles</code> y tiene la siguiente sintaxis:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>headerfiles <span class="o">[</span><span class="nt">-h</span> <span class="nt">--help</span> <span class="nt">-j</span> <span class="nt">-f</span> <nombre_de_fichero> <span class="nt">-e</span> <span class="o">[</span><span class="nt">-x</span>|-p] <span class="nt">-o</span> <span class="o">[</span>slow|fast]]
</code></pre></div></div>
<p>Particularidades:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">-x</code> y <code class="language-plaintext highlighter-rouge">-p</code> sólo están disponible si <code class="language-plaintext highlighter-rouge">-e</code> ha sido definido previamente.</li>
<li><code class="language-plaintext highlighter-rouge">-x</code> y <code class="language-plaintext highlighter-rouge">-p</code> son mutuamente excluyentes.</li>
<li><code class="language-plaintext highlighter-rouge">-f <nombre_de_fichero></code> puede ser especificado varias veces.</li>
<li><code class="language-plaintext highlighter-rouge">-j -e</code> no presentan problems si se indican varias veces, pero es equivalente a que sólo se indicasen una vez.</li>
<li><code class="language-plaintext highlighter-rouge">-o</code> sólo puede ser especificado una vez.</li>
<li><code class="language-plaintext highlighter-rouge">-h --help</code> tienen prioridad sobre cualquier otra opción.</li>
</ul>
<h3 id="el-script">El <em>script</em></h3>
<p>El primer paso será crear un fichero para guardar las funciones del <em>script</em>. Personalmente los suelo guardar en una ruta del tipo <code class="language-plaintext highlighter-rouge"><proyecto>/scripts/autocomplete/headerfiles.bash</code>, pero queda a vuestra discreción.</p>
<p>Hay, como es imaginable, varias formas de programar este autocompletado personalizado, pero mostraré la que a mí particurmente me resulta más sencilla mentalmente, aunque no necesariamente sea la más eficiente: definir una función que fije la variable de entorno <code class="language-plaintext highlighter-rouge">COMPREPLY</code>. y pasar dicha función como argumento de <code class="language-plaintext highlighter-rouge">complete</code>, junto con el nombre de nuestro comando (Bash usará este nombre para determinar si debe llamar a nuestro autocompletado particular o a algún otro). La variable <code class="language-plaintext highlighter-rouge">COMPREPLY</code> es usada por <code class="language-plaintext highlighter-rouge">complete</code> como la lista de opciones que se mostrarán. Como ayuda, usaremos el comando <code class="language-plaintext highlighter-rouge">compgen</code> para generar, de forma amigable, dicha lista de opciones.</p>
<p>Presento el <em>script</em> correspondiente a la sintaxis antes mencionada y luego procedo a su explicación:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#/usr/bin/env bash</span>
<span class="c"># Prints 1 if the given option has already been specified, 0 otherwise</span>
has_option<span class="o">()</span> <span class="o">{</span>
<span class="k">for </span>i <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">COMP_WORDS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$i</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">echo</span> <span class="s2">"1"</span>
<span class="k">return
fi
done
</span><span class="nb">echo</span> <span class="s2">"0"</span>
<span class="o">}</span>
<span class="c"># Function to be called when auto-completing</span>
_headerfiles<span class="o">()</span> <span class="o">{</span>
<span class="nv">COMPREPLY</span><span class="o">=()</span>
<span class="c"># These options have the highest precedence, so ignore any other if they've been specified</span>
<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="si">$(</span>has_option <span class="nt">-h</span><span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"1"</span> <span class="o">||</span> <span class="s2">"</span><span class="si">$(</span>has_option <span class="nt">--help</span><span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"1"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
return </span>0
<span class="k">fi
</span><span class="nb">local </span><span class="nv">cur</span><span class="o">=</span><span class="k">${</span><span class="nv">COMP_WORDS</span><span class="p">[COMP_CWORD]</span><span class="k">}</span>
<span class="nb">local </span><span class="nv">prev</span><span class="o">=</span><span class="k">${</span><span class="nv">COMP_WORDS</span><span class="p">[COMP_CWORD - 1]</span><span class="k">}</span>
<span class="k">case</span> <span class="nv">$prev</span> <span class="k">in</span>
<span class="c"># Options with additional arguments</span>
<span class="s2">"-f"</span><span class="p">)</span> <span class="nv">COMPREPLY</span><span class="o">=(</span><span class="sb">`</span><span class="nb">compgen</span> <span class="nt">-f</span> <span class="nt">--</span> <span class="nv">$cur</span><span class="sb">`</span><span class="o">)</span> <span class="p">;;</span>
<span class="s2">"-o"</span><span class="p">)</span> <span class="nv">COMPREPLY</span><span class="o">=(</span><span class="sb">`</span><span class="nb">compgen</span> <span class="nt">-W</span> <span class="s2">"slow fast"</span> <span class="nt">--</span> <span class="nv">$cur</span><span class="sb">`</span><span class="o">)</span> <span class="p">;;</span>
<span class="c"># Any other option</span>
<span class="k">*</span><span class="p">)</span>
<span class="c"># This variable will contain the list of available options</span>
<span class="nb">local </span><span class="nv">AVAILABLE_OPTIONS</span><span class="o">=()</span>
<span class="c"># List of supported options that can be used only once</span>
<span class="nb">local </span><span class="nv">ALL_ONCE_OPTIONS</span><span class="o">=(</span><span class="s2">"-e -o -h --help"</span><span class="o">)</span>
<span class="c"># Add dependant options</span>
<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="si">$(</span>has_option <span class="nt">-e</span><span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"1"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
<span class="c"># Mutually exclusive options</span>
<span class="c"># Do not remove current word in shell to allow finishing its autocompletion</span>
<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="si">$(</span>has_option <span class="nt">-x</span><span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"1"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="o">=(</span><span class="s2">"</span><span class="k">${</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="p">[0]</span><span class="k">}</span><span class="s2"> -x"</span><span class="o">)</span>
<span class="k">elif</span> <span class="o">[[</span> <span class="s2">"</span><span class="si">$(</span>has_option <span class="nt">-p</span><span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"1"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="o">=(</span><span class="s2">"</span><span class="k">${</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="p">[0]</span><span class="k">}</span><span class="s2"> -p"</span><span class="o">)</span>
<span class="k">else
</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="o">=(</span><span class="s2">"</span><span class="k">${</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="p">[0]</span><span class="k">}</span><span class="s2"> -x -p"</span><span class="o">)</span>
<span class="k">fi
fi</span>
<span class="c"># Most options are allowed only once, so remove the ones already in use,</span>
<span class="c"># but do not remove current word in shell to allow finishing its autocompletion</span>
<span class="nb">local </span><span class="nv">PREV_COMP_WORDS</span><span class="o">=(</span><span class="s2">"</span><span class="k">${</span><span class="nv">COMP_WORDS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="o">)</span>
<span class="nb">unset</span> <span class="s2">"PREV_COMP_WORDS[-1]"</span>
<span class="k">for </span>i <span class="k">in</span> <span class="k">${</span><span class="nv">ALL_ONCE_OPTIONS</span><span class="k">}</span><span class="p">;</span> <span class="k">do
for </span>j <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">PREV_COMP_WORDS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$i</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$j</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
continue </span>2
<span class="k">fi
done
</span>AVAILABLE_OPTIONS+<span class="o">=(</span><span class="s2">"</span><span class="nv">$i</span><span class="s2">"</span><span class="o">)</span>
<span class="k">done</span>
<span class="c"># The -f option can be used several times</span>
<span class="nv">AVAILABLE_OPTIONS</span><span class="o">=(</span><span class="s2">"</span><span class="k">${</span><span class="nv">AVAILABLE_OPTIONS</span><span class="p">[*]</span><span class="k">}</span><span class="s2"> -f"</span><span class="o">)</span>
<span class="nv">COMPREPLY</span><span class="o">=(</span><span class="sb">`</span><span class="nb">compgen</span> <span class="nt">-W</span> <span class="s2">"</span><span class="k">${</span><span class="nv">AVAILABLE_OPTIONS</span><span class="p">[*]</span><span class="k">}</span><span class="s2">"</span> <span class="nt">--</span> <span class="nv">$cur</span><span class="sb">`</span><span class="o">)</span>
<span class="p">;;</span>
<span class="k">esac</span>
<span class="o">}</span>
<span class="nb">complete</span> <span class="nt">-F</span> _headerfiles headerfiles
</code></pre></div></div>
<p>Como notas particulares:</p>
<ul>
<li>Las opciones de máxima prioridad, aquéllas que cuando se especifican dejan sin efecto a las demás, se procesan de primero y con un <em>early return</em>.</li>
<li>Las opciones con argumentos adicionales se procesan de forma independiente, pudiendo generar un autocompletado específico para dicha opción:
<ul>
<li><code class="language-plaintext highlighter-rouge">compgen -f</code>: nombres de ficheros.</li>
<li><code class="language-plaintext highlighter-rouge">compgen -d</code>: nombres de directorios.</li>
<li><code class="language-plaintext highlighter-rouge">compgen -W "..."</code>: una lista de palabras (nótese que éste es el mismo método empleado en otras partes del <em>script</em>).</li>
<li>La opción anterior puede usarse en conjunto con una función que extraiga los términos disponibles (de un fichero, de otro comando, etc). Por ejemplo, Git lo hace cuando detecta una línea tipo <code class="language-plaintext highlighter-rouge">git checkout</code>, entonces el siguiente autocompletado son nombres de las ramas disponibles, que son extraídas de una consulta a <code class="language-plaintext highlighter-rouge">git branch -a</code>.</li>
</ul>
</li>
<li>Cuando hay que listar las opciones disponibles, primero se enumeran las que se pueden elegir una única vez y se filtran para quitar las ya introducidas. Posteriormente se añaden las que pueden repetirse</li>
</ul>
<h3 id="activando-el-script-de-autocompletado">Activando el <em>script</em> de autocompletado</h3>
<p>Para activarlo bash con que carguemos el <em>script</em>: <code class="language-plaintext highlighter-rouge">source /path/to/script.bash</code>. Además, podemos agregar esta línea en nuestro <code class="language-plaintext highlighter-rouge">.bashrc</code> para que esté disponible en cualquier nueva sesión Bash que ejecutemos.</p>
<p>Si nuestro proyecto incluye un comando de instalación o un paquete, deberemos añadir el <em>script</em> en el mismo e instalarlo en <code class="language-plaintext highlighter-rouge">/usr/share/bash-completion/completions/</code>.</p>
<h2 id="conclusiones">Conclusiones</h2>
<p>Hemos estudiado cómo extender el autocompletado de Bash con algunas de las opciones más frecuentes. Otras combinaciones más complicadas pueden resolverse con una extensión de éstas (por ejemplo, el caso de que la lista de subvalores de una opción deba ser extraída de un fichero o comando). Para más información podemos consultar <a href="https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Programmable-Completion">la documentación oficial</a>.</p>Carlos BuchartExplicamos cómo extender la función de autocompletado de Bash para soportar nuestras propias aplicaciones.