Apéndice B. Directrices de programación

Este apéndice es una colección de sugerencias para programación con C++. Se han reunido a lo largo de mi experiencia como docente y programador y

también de las aportaciones de amigos como Dan Saks (coautor junto a Tom Plum de C++ Programming Guidelines, Plum Hall, 1991), Scott Meyers (autor de Effective C++, 2ª edición, Addison-Wesley, 1998) y Rob Murray (autor de C++ Strategies & Tactics, Addison-Wesley, 1993). Además, muchos de los consejos están resumidos a partir del contenido de Thinking in C++.

  1. Primero haga que funcione, después hágalo rápido. Esto es cierto incluso si está seguro de que un trozo de código es realmente importante y de que será el principal cuello de botella del sistema. No lo haga. Primero, consiga que el sistema funcione con un diseño lo más simple posible. Después, si no es suficientemente rápido, analícelo con un perfilador. Casi siempre descubrirá que «su» cuello de botella no es el problema. Reserve su tiempo para lo verdaderamente importante.

  2. La elegancia siempre vale la pena. No es un pasatiempo frívolo. No sólo hace que un programa sea más fácil de construir y depurar, sino también de comprender y mantener, y ahí es donde radica su valor económico. Esta cuestión puede requerir cierta experiencia para creérsela, porque puede parecer que mientras se está haciendo un trozo de código elegante, no se es productivo. La productividad aparece cuando el código se integra sin problemas en el sistema, y aún más cuando se modifica el código o el sistema.

  3. Recuerde el principio «divide y vencerás». Si el problema al que se enfrenta es demasiado confuso, intente imaginar cuál sería la operación básica del programa suponiendo que existe una «pieza» mágica que se encarga de las partes difíciles. Esa «pieza» es un objeto - escriba el código que usa el objeto, después implemente ese objeto encapsulando las partes difíciles en otros objetos, etc.

  4. No reescriba automáticamente todo su código C a C++ a menos que necesite cambiar significativamente su funcionalidad (es decir, no lo arregle si no está roto). Recompilar C en C++ es una actividad valiosa porque puede revelar errores ocultos. Sin embargo, tomar código C que funciona bien y reescribirlo en C++ puede no ser la mejor forma de invertir el tiempo, a menos que la versión C++ le ofrezca muchas oportunidades de reutilización como clase.

  5. Si tiene una gran cantidad de código C que necesite cambios, primero aísle las partes del código que no se modificarán, posiblemente envolviendo esas funciones en una «clase API» como métodos estáticos. Después céntrese en el código que va a cambiar, refactorizándolo en clases para facilitar las modificaciones en el proceso de mantenimiento.

  6. Separe al creador de la clase del usuario de la clase (el programador cliente). El usuario de la clase es el «consumidor» y no necesita ni quiere saber qué ocurre dentro de la clase. El creador de la clase debe ser un experto en diseño de clases y escribir la clase para que pueda ser usada por el programador más inexperto posible, y aún así funcionar de forma robusta en la aplicación. El uso de la librería será sencillo sólo si es transparente.

  7. Cuando cree una clase, utilice nombres tan claros como sea posible. El objetivo debería ser que la interfaz del programador cliente sea conceptualmente simple. Intente utilizar nombres tan claros que los comentarios sean innecesarios. Con este fin, use sobrecarga de funciones y argumentos por defecto para crear una interfaz intuitiva y fácil de usar.

  8. El control de acceso le permite (al creador de la clase) cambiar tanto como sea posible en el futuro sin afectar al código del cliente en el que se usa la clase. Desde este punto de vista, mantenga todo tan privado como sea posible, y haga pública solamente la interfaz de la clase, usando siempre métodos en lugar de atributos. Ponga atributos públicos sólo cuando se vea obligado. Si los usuarios de la clase no necesitan acceder a una función, hágala privada. Si una parte de su clase debe quedar expuesta a clases derivadas como protegida, proporcione una interfaz con funciones en lugar de exponer los datos reales. De este modo, los cambios en la implementación tendrán un impacto mínimo en las clases derivadas.

  9. No caiga en la parálisis por análisis. Hay algunas cosas que no aprenderá hasta que empiece a codificar y consiga que algún tipo de sistema funcione. C++ tiene mecanismos de seguridad de fábrica; déjelos trabajar por usted. Sus errores en una clase o conjunto de clases no destruirán la integridad del sistema completo.

  10. Su análisis y diseño deben producir, como mínimo, las clases del sistema, sus interfaces públicas, y las relaciones con otras clases, especialmente las clases base. Si su metodología de diseño produce más que eso, pregúntese si todas las piezas producidas por esa metodología tienen valor a lo largo de la vida del programa. Si no lo tienen, mantenerlas le costará. Los miembros de los equipos de desarrollo tienden a no mantener nada que no contribuya a su productividad; es un hecho que muchos métodos de diseño no tienen en cuenta.

  11. Escriba primero el código de las pruebas (antes de escribir la clase), y guárdelo junto a la clase. Automatice la ejecución de las pruebas con un makefile o herramienta similar. De este modo, cualquier cambio se puede verificar automáticamente ejecutando el código de prueba, lo que permite descubrir los errores inmediatamente. Como sabe que cuenta con la red de seguridad de su framework de pruebas, se atreverá a hacer cambios de gran alcance cuando descubra que son necesarios. Recuerde que las mejoras más importantes en los lenguajes provienen de las pruebas integradas que proporcionan la comprobación de tipos, la gestión de excepciones, etc., pero esas características sólo le llevan hasta cierto punto. Debe recorrer el resto del camino hacia un sistema robusto escribiendo las pruebas que verifiquen las características específicas de su clase o programa.

  12. Escriba primero el código de las pruebas (antes de escribir la clase) para verificar que el diseño de la clase está completo. Si no puede escribir el código de pruebas, significa que no sabe qué aspecto tiene la clase. Además, el hecho de escribir las pruebas a menudo desvela características adicionales o restricciones que necesita la clase - esas características o restricciones no siempre aparecen durante el análisis y diseño.

  13. Recuerde una regla fundamental de la ingeniería del software [91]: Todos los problemas del diseño de software se pueden simplificar introduciendo un nivel más de indirección conceptual. Esta única idea es la base de la abstracción, la principal cualidad de la programación orientada a objetos.

  14. Haga clases tan atómicas como sea posible; es decir, dé a cada clase un propósito único y claro. Si el diseño de su clase o de su sistema crece hasta ser demasiado complicado, divida las clases complejas en otras más simples. El indicador más obvio de esto es el tamaño: si una clase es grande, probablemente esté haciendo demasiado y debería dividirse.

  15. Vigile las definiciones de métodos largos. Una función demasiado larga y complicada es difícil y cara de mantener, y es probable que esté intentando hacer demasiado trabajo por sí misma. Si ve una función así, es señal de que, al menos, debería dividirse en varias funciones. También puede sugerir la creación de una nueva clase.

  16. Vigile las listas de argumentos largas. Las llamadas a función se vuelven difíciles de escribir, leer y mantener. En su lugar, intente mover el método a una clase donde sea más apropiado, y/o pásele objetos como argumentos.

  17. No se repita. Si un trozo de código se repite en muchos métodos de las clases derivadas, ponga el código en un método de la clase base e invóquelo desde las clases derivadas. No sólo ahorrará código, también facilitará la propagación de los cambios. Puede usar una función inline si necesita eficiencia. A veces el descubrimiento de este código común añadirá funcionalidad valiosa a su interfaz.

  18. Vigile las sentencias switch o cadenas de if-else. Son indicadores típicos de código dependiente del tipo, lo que significa que está decidiendo qué código ejecutar basándose en alguna información de tipo (el tipo exacto puede no ser obvio al principio). Normalmente puede reemplazar este tipo de código por herencia y polimorfismo; una llamada a una función polimórfica efectuará la comprobación de tipo por usted, y hará que el código sea más fiable y sencillo de extender.

  19. Desde el punto de vista del diseño, busque y separe las cosas que cambian de las que no cambian. Es decir, busque los elementos del sistema que podría querer cambiar sin forzar un rediseño, y después encapsule esos elementos en clases. Puede aprender mucho más sobre este concepto en el capítulo Design Patterns del Volumen 2 de este libro, disponible en www.BruceEckel.com [92]

  20. Tenga cuidado con la varianza. Dos objetos semánticamente diferentes pueden tener acciones o responsabilidades idénticas, y hay una tendencia natural a intentar hacer que uno sea subclase del otro sólo para beneficiarse de la herencia. Esto se llama varianza, pero no hay una justificación real para forzar una relación superclase/subclase donde no existe. Una solución mejor es crear una clase base general que proporcione una interfaz para ambas como clases derivadas; requiere un poco más de espacio, pero sigue beneficiándose de la herencia y probablemente hará un importante descubrimiento sobre el diseño.

  21. Tenga cuidado con la limitación durante la herencia. Los diseños más limpios añaden nuevas capacidades a las heredadas. Un diseño sospechoso elimina capacidades durante la herencia sin añadir otras nuevas. Pero las reglas están hechas para romperse, y si está trabajando con una librería de clases antigua, puede ser más eficiente restringir una clase existente en una subclase que reestructurar la jerarquía de modo que la nueva clase encaje donde debería, sobre la clase antigua.

  22. No extienda funcionalidad fundamental por medio de subclases. Si un elemento de la interfaz es esencial para una clase debería estar en la clase base, no añadido en una clase derivada. Si está añadiendo métodos por herencia, quizá debería repensar el diseño.

  23. Menos es más. Empiece con una interfaz mínima para la clase, tan pequeña y simple como necesite para resolver el problema que está tratando, pero no intente anticipar todas las formas en las que se podría usar la clase. A medida que se use la clase, descubrirá en qué sentidos debe ampliar la interfaz. Sin embargo, una vez que la clase esté siendo usada, no podrá reducir la interfaz sin causar problemas al código cliente. Si necesita añadir más funciones, está bien; eso no afecta al código, salvo porque obliga a recompilar. Pero incluso si los nuevos métodos reemplazan la funcionalidad de los antiguos, deje tranquila la interfaz existente (si lo desea, puede combinar la funcionalidad en la implementación subyacente). Si necesita expandir la interfaz de un método existente añadiendo más argumentos, deje los argumentos existentes en el orden actual, y ponga valores por defecto a todos los argumentos nuevos; de este modo no perturbará ninguna de las llamadas antiguas a esa función.

  24. Lea sus clases en voz alta para estar seguro de que suenan lógicas, refiriéndose a la relación entre una clase base y una clase derivada como «es-un» y a los objetos miembro como «tiene-un».

  25. Cuando tenga que decidir entre herencia y composición, pregúntese si necesita hacer upcast al tipo base. Si la respuesta es no, elija composición (objetos miembro) en lugar de herencia. Esto puede eliminar la aparente necesidad de herencia múltiple. Si hereda, los usuarios pensarán que se espera que hagan upcast.

  26. A veces, se necesita heredar para acceder a miembros protegidos de una clase base. Esto puede conducir a una aparente necesidad de herencia múltiple. Si no necesita hacer upcast, primero derive una nueva clase para efectuar el acceso protegido. Después haga que la nueva clase sea un objeto miembro dentro de cualquier clase que necesite usarla, en lugar de heredar.

  27. Por lo general, una clase base se usará principalmente para crear una interfaz para las clases que hereden de ella. De ese modo, cuando cree una clase base, haga que por defecto los métodos sean virtuales puros. El destructor puede ser también virtual puro (para obligar a las clases derivadas a redefinirlo explícitamente), pero recuerde poner al destructor un cuerpo, porque todos los destructores de la jerarquía se ejecutan siempre.

  28. Cuando ponga un método virtual en una clase, haga que todos los métodos de la clase sean también virtuales, y ponga un destructor virtual. Esta propuesta evita sorpresas en el comportamiento de la interfaz. Empiece a quitar la palabra reservada virtual sólo cuando esté intentando optimizar y su perfilador le haya apuntado en esa dirección.

  29. Use atributos para variaciones en los valores y métodos virtuales para variaciones en el comportamiento. Es decir, si encuentra una clase que usa atributos de estado junto con métodos que cambian de comportamiento basándose en esos atributos, probablemente debería rediseñarla para expresar las diferencias de comportamiento con subclases y métodos virtuales redefinidos.

  30. Si debe hacer algo no portable, cree una abstracción para ese servicio y localícela en una clase. Este nivel extra de indirección evita que la falta de portabilidad se extienda por todo el programa.

  31. Evite la herencia múltiple. Sirve para sacarle de situaciones difíciles, especialmente para reparar interfaces de clases cuando no tiene control sobre la clase defectuosa (vea el Volumen 2). Debería ser un programador experimentado antes de incluir herencia múltiple en el diseño de su sistema.

  32. No use herencia privada. Aunque está en el lenguaje y parece útil en ocasiones, introduce ambigüedades importantes cuando se combina con la identificación de tipos en tiempo de ejecución. Cree un objeto miembro privado en lugar de usar herencia privada.

  33. Si dos clases están asociadas entre sí de algún modo (como los contenedores y los iteradores), intente hacer que una de ellas sea una clase amiga pública anidada de la otra, tal como la Librería Estándar de C++ hace con los iteradores dentro de los contenedores (en la última parte del Capítulo 16 se muestran ejemplos de esto). No sólo pone de manifiesto la asociación entre las clases, también permite que el nombre de la clase se pueda reutilizar anidándola en otra clase. La Librería Estándar de C++ lo hace definiendo una clase iterador anidada dentro de cada clase contenedor, de ese modo los contenedores tienen una interfaz común. La otra razón por la que querrá anidar una clase es como parte de la implementación privada. En ese caso, el anidamiento es beneficioso para ocultar la implementación más que por la asociación de clases y la prevención de la contaminación del espacio de nombres citadas arriba.

  34. La sobrecarga de operadores es sólo «azúcar sintáctico»: una manera diferente de hacer una llamada a función. Si sobrecargar un operador no hace que la interfaz de la clase sea más clara o fácil de usar, no lo haga. Cree sólo un operador de conversión automática de tipo por clase. En general, siga las directrices y el estilo indicados en el Capítulo 12 cuando sobrecargue operadores.

  35. No sea una víctima de la optimización prematura. Ese camino lleva a la locura. En particular, no se preocupe de escribir (o evitar) funciones inline, hacer algunas funciones no virtuales o afinar el código para hacerlo más eficiente cuando esté en la primera fase de construcción del sistema. El objetivo principal debería ser probar el diseño, a menos que el propio diseño requiera cierta eficiencia.

  36. Normalmente, no deje que el compilador cree los constructores, destructores o el operator= por usted. Los diseñadores de clases siempre deberían decir qué debe hacer la clase exactamente y mantenerla enteramente bajo su control. Si no quiere constructor de copia u operator=, declárelos como privados. Recuerde que si crea algún constructor, el compilador no sintetizará un constructor por defecto.

  37. Si su clase contiene punteros, debe crear el constructor de copia, el operator= y el destructor de la clase para que funcione adecuadamente.

  38. Cuando escriba un constructor de copia para una clase derivada, recuerde llamar explícitamente al constructor de copia de la clase base (y también los de los objetos miembro). (Vea el Capítulo 14.) Si no lo hace, se invocará el constructor por defecto de la clase base (o del objeto miembro) y con mucha probabilidad eso no es lo que usted quiere. Para invocar el constructor de copia de la clase base, pásele el objeto derivado desde el que está copiando:

    Derived(const Derived& d) : Base(d) { // ...
    
  39. Cuando escriba un operador de asignación para una clase derivada, recuerde llamar explícitamente al operador de asignación de la clase base. (Vea el Capítulo 14.) Si no lo hace, no ocurrirá nada (lo mismo es aplicable a los objetos miembro). Para invocar el operador de asignación de la clase base, use el nombre de la clase base y el operador de resolución de ámbito:

    Derived& operator=(const Derived& d) {
        Base::operator=(d);
    
  40. Si necesita minimizar las recompilaciones durante el desarrollo de un proyecto grande, use la técnica de las clases manejador o «gato de Cheshire» que se mostró en el Capítulo 5, y elimínela sólo si la eficiencia en tiempo de ejecución es un problema.

  41. Evite el preprocesador. Use siempre const para la substitución de valores y funciones inline en lugar de macros.

  42. Mantenga los ámbitos tan pequeños como sea posible de modo que la visibilidad y el tiempo de vida de los objetos sean lo más reducidos posible. Esto reduce el peligro de usar un objeto en el contexto equivocado y de ocultar un error difícil de encontrar. Por ejemplo, suponga que tiene un contenedor y un trozo de código que itera sobre él. Si copia el código para usarlo con otro contenedor, puede que accidentalmente acabe usando el tamaño del primer contenedor como el límite superior del nuevo. Pero si el primer contenedor estuviese fuera de ámbito, el error se detectaría en tiempo de compilación.

  43. Evite las variables globales. Esfuércese siempre en poner los datos dentro de clases. Es más probable que aparezcan funciones globales de forma natural que variables globales, aunque puede que después descubra que una función global encaja mejor como método estático de una clase.

  44. Si necesita declarar una clase o función de una librería, hágalo siempre incluyendo su fichero de cabecera. Por ejemplo, si quiere crear una función para escribir en un ostream, no declare nunca el ostream por usted mismo, usando una especificación de tipo incompleta como ésta:

    class ostream;
    

    Este enfoque hace que su código sea vulnerable a cambios en la representación. (Por ejemplo, ostream podría ser en realidad un typedef.) En lugar de lo anterior, use siempre el fichero de cabecera:

    #include <iostream>
    

    Cuando cree sus propias clases, si una librería es grande, proporcione a sus usuarios una versión abreviada del fichero de cabecera con especificaciones de tipo incompletas (es decir, declaraciones de los nombres de las clases) para los casos en que sólo necesiten usar punteros. (Eso puede acelerar las compilaciones.)

  45. Cuando elija el tipo de retorno de un operador sobrecargado, considere qué ocurrirá si se encadenan expresiones. Retorne una copia o referencia al valor izquierdo (return *this) de modo que se pueda usar en una expresión encadenada (A = B = C). Cuando defina el operator=, recuerde el caso x=x.

  46. Cuando escriba una función, pase los argumentos por referencia constante como primera elección. Siempre que no necesite modificar el objeto que está pasando, ésta es la mejor práctica porque es tan simple como si lo pasara por valor pero sin pagar el alto precio de construir y destruir un objeto local, que es lo que ocurre cuando se pasa por valor. Normalmente no querrá preocuparse demasiado por las cuestiones de eficiencia cuando esté diseñando y construyendo su sistema, pero este hábito es una apuesta segura.

  47. Tenga cuidado con los temporarios. Cuando esté optimizando, busque creaciones de temporarios, especialmente con sobrecarga de operadores. Si sus constructores y destructores son complicados, el coste de la creación y destrucción de temporarios puede ser muy alto. Cuando devuelva un valor desde una función, intente siempre construir el objeto «en el sitio» (in place) con una llamada al constructor en la sentencia de retorno:

    return MyType(i, j);
    

    mejor que

    MyType x(i, j);
    return x;
    

    La primera sentencia return (la llamada optimización del valor de retorno) evita una llamada al constructor de copia y al destructor.

  48. Cuando escriba constructores, considere las excepciones. En el mejor caso, el constructor no hará nada que lance una excepción. En el segundo mejor caso, la clase estará compuesta sólo por clases robustas y heredará sólo de ellas, de modo que éstas se limpiarán automáticamente si se lanza una excepción. Si necesita punteros desnudos, usted es responsable de capturar sus propias excepciones y de liberar los recursos apuntados antes de lanzar una excepción en su constructor. Si un constructor tiene que fallar, la acción apropiada es lanzar una excepción.

  49. En los constructores, haga lo mínimo necesario. No sólo se reduce así la sobrecarga de las llamadas a los constructores (muchas de las cuales pueden quedar fuera de su control), sino que además será menos probable que sus constructores lancen excepciones o causen problemas.

  50. La responsabilidad del destructor es la de liberar los recursos solicitados durante la vida del objeto, no sólo durante la construcción.

  51. Utilice jerarquías de excepciones, preferiblemente derivadas de la jerarquía de excepciones estándar de C++, y anídelas como clases públicas dentro de la clase que lanza la excepción. Así, quien captura las excepciones puede capturar primero los tipos específicos de excepciones y después el tipo base. Si añade una nueva excepción derivada, el código cliente anterior seguirá capturando la excepción por medio del tipo base.

  52. Lance las excepciones por valor y captúrelas por referencia. Deje que el mecanismo de gestión de excepciones haga la gestión de memoria. Si lanza punteros a objetos excepción creados en el montículo, quien capture la excepción debe saber que tiene que destruirla, lo cual implica un acoplamiento perjudicial. Si captura las excepciones por valor, provocará construcciones y destrucciones adicionales; peor aún, las partes derivadas de sus objetos excepción pueden quedar truncadas al hacer upcasting por valor.

  53. No escriba sus propias clases plantilla a menos que deba. Mire primero en la Librería Estándar de C++, después en los proveedores que crean herramientas de propósito específico. Adquiera habilidad en su uso y conseguirá incrementar mucho su productividad.

  54. Cuando cree plantillas, busque el código que no dependa del tipo y póngalo en una clase base no-plantilla para evitar que el código aumente de tamaño sin necesidad. Por medio de herencia o composición, puede crear plantillas en las que la mayor parte del código que contienen depende del tipo y, por tanto, es esencial.

  55. No use las funciones de <cstdio>, como por ejemplo printf(). Aprenda a usar iostreams en su lugar; tienen comprobación de tipos, son extensibles a nuevos tipos y son mucho más potentes. El esfuerzo se verá recompensado con regularidad. En general, use siempre librerías C++ antes que librerías C.

  56. Evite los tipos predefinidos de C. C++ los admite por compatibilidad con C, pero son tipos mucho menos robustos que las clases C++, de modo que pueden complicar la depuración.

  57. Siempre que use tipos predefinidos para variables globales o automáticas, no los defina hasta que pueda inicializarlos. Defina una variable por línea, junto con su inicialización. Cuando defina punteros, ponga el '*' al lado del nombre del tipo. Puede hacerlo de forma segura si define una variable por línea. Este estilo suele resultar menos confuso para el lector.

  58. Garantice que tiene lugar la inicialización en todos los aspectos de su programa. Inicialice todos los atributos en la lista de inicialización del constructor, incluso los tipos predefinidos (usando los pseudoconstructores). Usar la lista de inicialización del constructor es normalmente más eficiente cuando se inicializan subobjetos; si no se hace se invocará el constructor por defecto, y acabará llamando a otros métodos (probablemente el operator=) para conseguir la inicialización que desea.

  59. No use la forma MyType a = b; para definir un objeto. Ésta es una de las mayores fuentes de confusión porque llama a un constructor en lugar de al operator=. Por motivos de claridad, sea específico y use mejor la forma MyType a(b);. Los resultados son idénticos, pero el lector no se podrá confundir.

  60. Use los moldes explícitos descritos en el Capítulo 3. Un molde anula el sistema normal de tipos y es un posible punto de error. Como los moldes explícitos dividen el molde único de C, que lo hace todo, en clases de moldes bien diferenciados, cualquiera que depure o mantenga el código podrá encontrar fácilmente todos los sitios en los que es más probable que sucedan errores lógicos.

  61. Para que un programa sea robusto, cada componente debe ser robusto. Use todas las herramientas que proporciona C++: control de acceso, excepciones, uso correcto de const, comprobación de tipos, etc., en cada clase que cree. De ese modo podrá pasar de una forma segura al siguiente nivel de abstracción cuando construya su sistema.

  62. Haga desde el principio un uso correcto de const. Esto permite que el compilador advierta de errores que de otro modo serían sutiles y difíciles de encontrar. Esta práctica requiere cierta disciplina y se debe usar de modo coherente en todas sus clases, pero merece la pena.

  63. Use la comprobación de errores del compilador en su beneficio. Haga todas las compilaciones con todos los avisos habilitados y arregle el código para eliminar todas las advertencias. Escriba código que aproveche los errores y advertencias de compilación en lugar de código que provoque errores en tiempo de ejecución (por ejemplo, no use listas de argumentos variables, que desactivan toda la comprobación de tipos). Use assert() para depurar, pero use excepciones para los errores de ejecución.

  64. Son preferibles los errores de compilación a los de ejecución. Intente manejar un error tan cerca del punto donde ocurre como sea posible. Es mejor tratar el error en ese punto que lanzar una excepción. Capture cualquier excepción en el manejador más cercano que tenga suficiente información para tratarla. Haga lo que pueda con la excepción en el nivel actual; si no puede resolver el problema, relance la excepción. (Vea el Volumen 2 si necesita más detalles.)

  65. Si está usando especificaciones de excepciones (vea el Volumen 2 de este libro, disponible en www.BruceEckel.com, para aprender sobre la gestión de excepciones), instale su propia función unexpected() usando set_unexpected(). Su unexpected() debería registrar el error y relanzar la excepción actual. De ese modo, si una función existente es redefinida y empieza a lanzar excepciones, dispondrá de un registro del culpable y podrá modificar el código que la invoca para manejar la excepción.

  66. Cree una función terminate() definida por el usuario (indicando un error del programador) para registrar el error que causó la excepción, después libere los recursos del sistema, y termine el programa.

  67. Si un destructor llama a cualquier función, esas funciones podrían lanzar excepciones. Un destructor no puede lanzar una excepción (eso podría ocasionar una llamada a terminate(), lo que indica un error de programación), así que cualquier destructor que llame a otras funciones debe capturar y tratar sus propias excepciones.

  68. No «decore» los nombres de sus atributos privados (poniendo guiones bajos, notación húngara, etc.), a menos que tenga un montón de valores globales ya existentes; en cualquier otro caso, deje que las clases y los espacios de nombres definan el ámbito de los nombres por usted.

  69. Ponga atención a la sobrecarga. Una función no debería ejecutar código condicionalmente basándose en el valor de un argumento, sea por defecto o no. En su lugar, debería crear dos o más métodos sobrecargados.

  70. Oculte sus punteros dentro de clases contenedor. Sáquelos sólo cuando vaya a operar con ellos inmediatamente. Los punteros han sido siempre una de las mayores fuentes de errores. Cuando use new, intente colocar el puntero resultante en un contenedor. Es preferible que un contenedor «posea» sus punteros y sea responsable de la limpieza. Incluso mejor, envuelva un puntero dentro de una clase; si aún así quiere que parezca un puntero, sobrecargue operator-> y operator*. Si necesita tener un puntero normal, inicialícelo siempre, preferiblemente con la dirección de un objeto, o cero si es necesario. Asígnele cero cuando lo libere para evitar liberaciones múltiples accidentales.

  71. No sobrecargue los new y delete globales. Hágalo siempre en cada clase. Sobrecargar las versiones globales afecta al proyecto completo del programador cliente, algo que sólo los creadores del proyecto deberían controlar. Cuando sobrecargue new y delete en las clases, no asuma que conoce el tamaño del objeto; alguien puede heredar de esa clase. Use el argumento proporcionado. Si hace algo especial, considere el efecto que podría tener en las clases derivadas.

  72. Evite el truncado de objetos. Prácticamente nunca tiene sentido hacer upcast de un objeto por valor. Para evitar el upcast por valor, use métodos virtuales puros en su clase base.

  73. A veces la agregación simple resuelve el problema. Un «sistema de confort del pasajero» en una línea aérea consta de elementos inconexos: asiento, aire acondicionado, vídeo, etc., y sin embargo necesita crear muchos de ellos en un avión. ¿Debe crear miembros privados y construir una nueva interfaz completa? No - en este caso, los componentes también son parte de la interfaz pública, así que debería crear objetos miembro públicos. Esos objetos tienen sus propias implementaciones privadas, que siguen siendo seguras. Sea consciente de que la agregación simple no es una solución que deba usarse a menudo, pero a veces ocurre.



[91] Que me explicó Andrew Koenig.

[92] (N. del T.) Está prevista la traducción del Volumen 2 por parte del mismo equipo que ha traducido este volumen. Visite https://github.com/CRySoL/pensarencpp.