program - how to implement classes in c++
¿Uso a gran escala de los consejos de Meyer para preferir las funciones de no miembros, no amigos? (8)
(No tengo tiempo para escribir esto bien, el siguiente es un volcado de cerebro de 5 minutos que sin duda puede ser desgarrado en varios niveles de trival, pero por favor dirígete a los conceptos y al empuje general.)
Tengo una gran simpatía por la posición tomada por Jonathan Grynspan, pero quiero decir algo más de lo que razonablemente se puede hacer en los comentarios.
Primero, un "bien dicho" para Alf Steinbach, quien contribuyó con "Es solo una caricatura demasiado simplificada de sus puntos de vista que podría estar en conflicto. Por lo que vale, no estoy de acuerdo con Scott Meyers en este asunto; Veo que está generalizando demasiado aquí, o lo estaba haciendo ".
Scott, Herb, etc. estaban haciendo estos puntos cuando pocas personas entendían las concesiones o alternativas, y lo hicieron con una fuerza desproporcionada. Se analizaron algunas molestias persistentes que tenían las personas durante la evolución del código y se derivó racionalmente un nuevo enfoque de diseño que abordaba estos problemas . Volvamos a la pregunta de si hubo inconvenientes más adelante, pero primero - vale la pena decir que el dolor en cuestión era típicamente pequeño e infrecuente: las funciones no miembro son solo un pequeño aspecto del diseño de código reutilizable, y en los sistemas de escala empresarial que tengo trabajé simplemente escribiendo el mismo tipo de código que hubiera puesto en una función de miembro ya que un no miembro rara vez es suficiente para hacer que los no miembros sean reutilizables. Es muy raro que incluso expresen algoritmos que sean lo suficientemente complejos como para valer la pena reutilizarlos y que no estén estrechamente vinculados a los específicos de la clase para la que fueron diseñados, ya que es bastante extraño que sea prácticamente inconcebible que haya otra clase apoyando el mismas operaciones y semántica. A menudo, también necesita plantillas de argumentos o introducir una clase base para abstraer el conjunto de operaciones requeridas. Ambos tienen implicaciones significativas en términos de rendimiento, ya sea en línea versus fuera de línea, recompilación del código del cliente.
Dicho esto, a menudo hay menos cambios de código y se requiere un estudio de impacto al cambiar la implementación si las operaciones se han estado implementando en términos de una interfaz pública, y ser un no miembro no amigo sistemáticamente impone eso. Sin embargo, ocasionalmente hace que la implementación inicial sea más detallada o de alguna otra manera menos deseable y mantenible.
Pero, como prueba de fuego, ¿cuántas de estas funciones no miembro se encuentran en el mismo encabezado que la única clase para la que son actualmente aplicables? ¿Cuántos quieren abstraer sus argumentos a través de plantillas (lo que significa inlineación, dependencias de compilación) o clases base (gastos generales de funciones virtuales) para permitir la reutilización? Ambos disuaden a las personas de verlos como reutilizables, pero cuando no es el caso, las operaciones disponibles en una clase se deslocalizan , lo que puede frustrar la percepción de un sistema por parte de los desarrolladores: el desarrollo a menudo tiene que resolver el hecho bastante decepcionante de que "oh - eso solo funcionará para la clase X ".
En pocas palabras: la mayoría de las funciones miembro no son potencialmente reutilizables. Gran parte del código corporativo no se divide en algoritmos limpios en comparación con los datos con potencial para la reutilización de los primeros. Ese tipo de división simplemente no es necesario ni útil ni concebiblemente útil 20 años después. Es muy similar a los métodos get / set: se necesitan en ciertos límites de la API, pero pueden constituir una verbosidad innecesaria cuando se localiza la propiedad y el uso del código.
Personalmente, no tengo un enfoque de todo o nada para esto, pero decido qué hacer que un miembro funcione o no como miembro en función de si hay algún beneficio probable para cualquiera, posibilidad de reutilización versus localidad de interfaz.
Durante algún tiempo he estado diseñando las interfaces de mi clase para que sean mínimas, prefiriendo las funciones no miembro envueltas en el espacio de nombres sobre las funciones de los miembros. Esencialmente siguiendo el consejo de Scott Meyer en el artículo Cómo las funciones no miembro mejoran la encapsulación .
He estado haciendo esto con buen efecto en algunos proyectos a pequeña escala, pero me pregunto qué tan bien funciona en una escala mayor. ¿Hay algún proyecto grande y bien considerado de código abierto de C ++ que pueda ver y tal vez hacer referencia a dónde se siguen estos consejos?
Actualización: Gracias por todas las aportaciones, pero no estoy realmente interesado en la opinión, sino en descubrir qué tan bien funciona en la práctica a una escala mayor. La respuesta de Nick es la más cercana a este respecto, pero me gustaría poder ver el código. Cualquier clase de descripción detallada de experiencias prácticas (positivos, negativos, consideraciones prácticas, etc.) sería aceptable también.
Como se indica en el artículo, STL tiene funciones miembro y no miembro. Esto no se debe a su preferencia, principalmente porque muchas de las funciones gratuitas operan en iteradores y los iteradores no están en una jerarquía de clases (porque STL quiere que los punteros en las matrices sean iteradores de primera clase).
Estoy totalmente en desacuerdo con este artículo para C ++, porque la inconsistencia sería molesta, y en la inteligencia moderna del IDE se rompería.
En C #, sin embargo, con los métodos de extensión, este es un muy buen consejo. Allí, obtienes lo mejor de ambos mundos: puedes hacer funciones no miembro que pueden parecer ser funciones miembro. También pueden estar en archivos separados, obteniendo todos los beneficios de esta práctica sin el inconveniente de inconsistencia.
Hago esto bastante en el proyecto en el que trabajo; el más grande de los cuales en mi empresa actual es de alrededor de 2 millones de líneas, pero no es de código abierto, por lo que no puedo proporcionarlo como referencia. Sin embargo, diré que estoy de acuerdo con el consejo, en términos generales. Cuanto más separes la funcionalidad que no está estrictamente contenida en un solo objeto de ese objeto, mejor será tu diseño.
A modo de ejemplo, considere el ejemplo del polimorfismo clásico: una clase base Shape con subclases y una función virtual Draw (). En el mundo real, Draw () necesitaría tomar algún contexto de dibujo, y potencialmente ser consciente del estado de otras cosas que se dibujan, o de la aplicación en general. Una vez que coloque todo eso en cada implementación de la subclase de Draw (), es probable que tenga una superposición de código, o la mayoría de su lógica de Draw () real estará en la clase base, o en otro lugar. Luego, considere que si desea volver a utilizar parte de ese código, deberá proporcionar más puntos de entrada en la interfaz y posiblemente contaminar las funciones con otro código que no esté relacionado con las formas de dibujo (por ejemplo: lógica de correlación de dibujo de varias formas) ) En poco tiempo, será un desastre, y desearás haber tenido una función de dibujo que tomó una forma (y contexto, y otros datos) en su lugar, y Shape solo tenía funciones / datos que estaban completamente encapsulados y que no usaban o referenciaban objetos externos
De todos modos, esa es mi experiencia / consejo, por lo que vale.
La biblioteca OpenCV hace esto. Tienen una clase cv :: Mat que presenta una matriz 3D (o imágenes). Luego tienen todas las otras funciones en el espacio de nombres cv.
La biblioteca OpenCV es enorme y es ampliamente considerada en este campo.
También hago esto mucho, donde parece tener sentido, y no causa absolutamente ningún problema con la escala. (aunque mi proyecto actual es solo 40000 LOC) De hecho, creo que hace que el código sea más escalable, rebaja las clases y reduce las dependencias. En ocasiones, es necesario que refactorice sus funciones para que sean independientes de los miembros de la clase y, por lo tanto, suele crear una biblioteca de funciones auxiliares más generales, que puede reutilizar fácilmente en cualquier otro lugar. También mencionaría que uno de los problemas más comunes en muchos proyectos grandes es la hinchazón de las clases, y creo que preferir las funciones que no son miembros o que no son amigos también ayuda aquí.
Una ventaja práctica de las funciones de escritura como no amigos que no son miembros es que al hacerlo puede reducir significativamente el tiempo que se tarda en probar y verificar el código.
Considere, por ejemplo, que el miembro del contenedor de secuencia funciona insert
y push_back
. Hay al menos dos enfoques para implementar push_back
:
- Simplemente puede llamar a
insert
(su comportamiento se define en términos deinsert
todos modos) - Puede hacer todo el trabajo que haría la
insert
(posiblemente llamando a funciones auxiliares privadas) sin llamar realmente a lainsert
Obviamente, al implementar un contenedor de secuencia, es probable que desee utilizar el primer enfoque. push_back
es solo una forma especial de insert
y (según mi leal saber y entender) no se puede obtener ningún beneficio de rendimiento implementando push_back
otra manera (al menos no para list
, deque
o vector
).
Sin embargo, para probar minuciosamente dicho contenedor, debe probar push_back
separado: dado que push_back
es una función miembro, puede modificar todo el estado interno del contenedor. Desde el punto de vista de la prueba, debe (¿debe?) Asumir que push_back
se implementa utilizando el segundo enfoque porque es posible que se pueda implementar utilizando el segundo enfoque. No hay garantía de que se implemente en términos de insert
.
Si push_back
se implementa como un no amigo no miembro, no puede tocar el estado interno del contenedor; debe usar el primer acercamiento. Cuando escribe pruebas para ello, sabe que no puede romper el estado interno del contenedor (suponiendo que las funciones reales del miembro del contenedor se implementen correctamente). Puede utilizar ese conocimiento para reducir significativamente la cantidad de pruebas que necesita escribir para ejercitar plenamente el código.
Yo argumentaría que el beneficio de las funciones que no son miembros aumenta a medida que aumenta el tamaño del proyecto. La biblioteca de contenedores, iteradores y algoritmos estándar de la biblioteca son prueba de esto.
Si puede desacoplar algoritmos de estructuras de datos (o, para decirlo de otra manera, si puede desacoplar lo que hace con objetos de cómo se manipula su estado interno), puede disminuir el acoplamiento entre sus clases y aprovechar mejor el código genérico.
Scott Meyers no es el único autor que ha argumentado a favor de este principio; Herb Sutter también lo ha hecho, especialmente en Monoliths Unstrung , que termina con la pauta:
Donde sea posible, prefiera escribir funciones como no amigos que no sean miembros.
Creo que uno de los mejores ejemplos de una función miembro std::basic_string::find
de ese artículo es std::basic_string::find
; no hay ninguna razón para que exista, ya que std::find
proporciona exactamente la misma funcionalidad.
Prefiere las funciones de no amigos no miembros para la encapsulación A MENOS que desee que las conversiones implícitas funcionen para las funciones de las plantillas de clase que no son miembros (en cuyo caso será mejor que las asocie):
Es decir, si tiene una plantilla de clase, type<T>
:
template<class T>
struct type {
void friend foo(type<T> a) {}
};
y un tipo implícitamente convertible a type<T>
, por ejemplo:
template<class T>
struct convertible_to_type {
operator type<T>() { }
};
El siguiente funciona como se espera:
auto t = convertible_to_type<int>{};
foo(t); // t is converted to type<int>
Sin embargo, si haces foo
una función no amiga :
template<class T>
void foo(type<T> a) {}
entonces lo siguiente no funciona:
auto t = convertible_to_type<int>{};
foo(t); // FAILS: cannot deduce type T for type
Como no puede deducir T
, la función foo
se elimina del conjunto de resolución de sobrecarga, es decir: no se encuentra ninguna función, lo que significa que la conversión implícita no se activa.