29

Aunque desconcertante con algunos hechos sobre el diseño de clases, específicamente si las funciones deberían ser miembros o no, busqué en C++ efectivo y encontré el Ítem 23, es decir, prefiero funciones no miembro no miembro a funciones miembro. Leerlo de primera mano con el ejemplo del navegador web tiene cierto sentido, sin embargo, las funciones de conveniencia (llamadas funciones no miembro como esta en el libro) en ese ejemplo cambian el estado de la clase, ¿verdad?C++ efectivo Artículo 23 Prefiere funciones no miembro no miembro a funciones miembro

  • Así que, primera pregunta, ¿no deberían ser miembros que?

  • Leyendo un poco más, considera que las funciones STL y, de hecho, algunas funciones que no están implementadas por algunas clases se implementan en stl. Siguiendo las ideas del libro, evolucionan hacia algunas funciones de conveniencia que se empaquetan en algunos espacios de nombres razonables, como std::sort, std::copy de algorithm. Por ejemplo, la clase vector no tiene una función sort y una usa la función stl sort, por lo que no es miembro de la clase vectorial. Pero también se podría estirar el mismo razonamiento a otras funciones en la clase vector como assign, por lo que tampoco podría implementarse como miembro sino como una función de conveniencia. Sin embargo, eso también cambia el estado interno del objeto, como el género en el que operaba. Entonces, ¿cuál es la razón de ser de este tema sutil pero importante (supongo)?

Si tiene acceso al libro, ¿puede aclararme un poco más estos puntos?

+5

Estoy sorprendido de que nadie ha escrito el [enlace a la muy relevante Dr. Dobbs artículo] (http: // drdobbs.com/cpp/184401197) por Scott Meyer todavía! – Xeo

Respuesta

29

El acceso al libro no es necesario.

Los temas que estamos tratando aquí son Dependencia y reutilización.

En un software bien diseñado, intenta aislar elementos entre sí para reducir las Dependencias, porque las Dependencias son un obstáculo que hay que superar cuando es necesario un cambio.

En un software bien diseñado, se aplica el principio de DRY (Do not Repeat Yourself) porque cuando es necesario un cambio, es doloroso y propenso a errores a tener que repetirlo en una docena de lugares diferentes.

La mentalidad de OO "clásica" es cada vez peor en el manejo de dependencias. Al tener muchos y muchos métodos que dependen directamente de las partes internas de la clase, el más mínimo cambio implica una reescritura completa. No es necesario que sea así.

En C++, la STL (no toda la biblioteca estándar), ha sido diseñada con los objetivos explícitos de:

  • dependencias de corte
  • permitiendo la reutilización

Por lo tanto, los recipientes se exponen interfaces bien definidas que ocultan sus representaciones internas pero aún ofrecen suficiente acceso a la información que encapsulan para que los algoritmos se puedan ejecutar en ellas. Todas las modificaciones se realizan a través de la interfaz del contenedor para que las invariantes estén garantizadas.

Por ejemplo, si piensa en los requisitos del algoritmo sort. Para la ejecución utilizado (en general) por el STL, se requiere (del contenedor):

  • acceso eficiente a un elemento de un índice determinado: Random Access
  • la capacidad de intercambiar dos elementos: no asociativos

Por lo tanto, cualquier contenedor que proporciona acceso aleatorio y no es asociativo es (en teoría) adecuado para ser clasificado de manera eficiente por (digamos) un algoritmo de clasificación rápida.

¿Qué son los Contenedores en C++ que satisfacen esto?

  • la básica C-array
  • deque
  • vector

Y cualquier recipiente que puede escribir si se presta atención a estos detalles.

Sería un desperdicio, ¿no sería así, volver a escribir (copiar/pegar/ajustar) sort para cada uno de esos?

Tenga en cuenta, por ejemplo, que existe un método std::list::sort. Por qué ? Porque std::list no ofrece acceso aleatorio (informalmente myList[4] no funciona), por lo tanto, el sort del algoritmo no es adecuado.

1

Creo que el género no está implementado como una función de miembro porque es ampliamente utilizado, no solo para vectores. Si lo tenían como función de miembro, tendrían que volver a implementarlo cada vez para cada contenedor que lo utiliza. Entonces creo que es para una implementación más fácil.

11

Creo que la razón de esta regla es que mediante el uso de funciones miembro puede confiar demasiado en las partes internas de una clase por accidente. Cambiar el estado de una clase no es un problema. El verdadero problema es la cantidad de código que necesita cambiar si modifica alguna propiedad privada dentro de su clase. Mantener la interfaz de la clase (métodos públicos) lo más pequeña posible reduce tanto la cantidad de trabajo que tendrá que hacer en ese caso como el riesgo de hacer algo extraño con sus datos privados, dejándolo con una instancia en un estado inconsistente .

AtoMerZ también es correcto, las funciones de no amigos que no son miembros se pueden modelar y reutilizar para otros tipos también.

Por cierto, usted debe comprar su copia de Effective C++, es un gran libro, pero no intente cumplir siempre con cada elemento de este libro. Diseño orientado a objetos, buenas prácticas (de libros, etc.) Y experiencia (creo que también está escrito en C++ efectivo en alguna parte).

+1

y no siempre siguen las pautas de Diseño Orientado a Objetos en C++, es multi-paradigma, por lo que algunas cosas se expresan mejor de lo contrario. –

17

El criterio que uso es si una función se podría implementar de manera significativamente más eficiente por ser una función miembro, entonces debería ser una función miembro. ::std::sort no cumple con esa definición. De hecho, no existe una diferencia de eficiencia en la implementación externa o interna.

Una gran mejora de eficiencia implementando algo como una función de miembro (o amigo) significa que se beneficia enormemente al conocer el estado interno de la clase.

Parte del arte del diseño de interfaz es el arte de encontrar el conjunto mínimo de funciones de miembros, de modo que todas las operaciones que desee realizar en el objeto se puedan implementar de manera razonablemente eficiente en términos de ellas.Y este conjunto no debería admitir operaciones que no deberían realizarse en la clase. Entonces no puedes simplemente implementar un montón de funciones getter y setter y llamarlo bueno.

+2

+1 para "no debería admitir operaciones que no deberían realizarse" –

2

La motivación es simple: mantener una sintaxis coherente. A medida que evoluciona o se usa la clase , aparecerán varias funciones de conveniencia que no son miembros, ; no desea modificar la interfaz de clase para agregar algo como toUpper a una clase de cadena, por ejemplo. (En el caso de std::string, por supuesto, no se puede.) Preocupación de Scott es que cuando este sucede, se termina con la sintaxis inconsistentes:

s.insert("abc"); 
toUpper(s); 

utilizando sólo las funciones gratuitas, declarándolos amigo como necesario, todas las funciones tienen la misma sintaxis. La alternativa sería modificar la definición de clase cada vez que agregue una función de conveniencia.

No estoy del todo convencido. Si una clase está bien diseñada, tiene una funcionalidad básica , es claro para el usuario qué funciones son parte de esa funcionalidad básica, y cuáles son funciones de conveniencia adicionales (si las hay). Globalmente, string es una especie de caso especial, porque está diseñado para ser utilizado para resolver muchos problemas diferentes; No me puedo imaginar que este sea el caso para muchas clases.

+0

¿Podría volver a formular "A medida que la clase evoluciona o se utiliza, varias funciones de conveniencia no miembro aparecerán, no desea modificar la interfaz de clase para agregar algo como toUpper en una clase de cuerda, por ejemplo. (En el caso de std :: string, por supuesto, no puedes). La preocupación de Scott es que cuando esto sucede, terminas con una sintaxis inconsistente: "toUpper parece gustarle". un miembro, lo que hace que una función de conveniencia no sea correcta, ¿correcto? –

+0

@Umut Sí. Por 'función de conveniencia', más o menos quería decir cualquier función que se agregó más tarde, que no requería acceso a los miembros privados de la clase. El problema es simplemente permitir que tales funciones adicionales utilicen la misma sintaxis de llamadas, de modo que un usuario posterior no tenga que distinguir qué se agregó y qué fue original. –

+0

¿Qué quiere decir con 'misma sintaxis de llamada' –

3

Así que, primera pregunta, ¿no deberían ser miembros que?

No, esto no sigue. En el diseño idiomático de la clase C++ (al menos, en las expresiones idiomáticas utilizadas en Effective C++), las funciones no miembro no miembro extienden la interfaz de la clase. Se pueden considerar parte de la API pública para la clase, a pesar de que no necesitan y no tienen acceso privado a la clase. Si este diseño no es "OOP" por alguna definición de OOP entonces, OK, C++ idiomático no es OOP por esa definición.

tramo el mismo razonamiento a otros funciones en la clase de vectores

eso es cierto, hay algunas funciones miembro de contenedores estándar que podrían haber sido las funciones gratuitas. Por ejemplo, vector::push_back se define en términos de insert, y ciertamente podría implementarse sin acceso privado a la clase. En ese caso, sin embargo, push_back es parte de un concepto abstracto, el BackInsertionSequence, ese vector se implementa. Tales conceptos genéricos abarcan el diseño de clases particulares, por lo tanto, si está diseñando o implementando sus propios conceptos genéricos que podrían influir en dónde coloca las funciones.

Ciertamente, hay partes de la norma que posiblemente deberían haber sido diferentes, por ejemplo std::string has way too many member functions. Pero lo que está hecho está hecho, y estas clases fueron diseñadas antes de que la gente realmente se estableciera en lo que ahora podríamos llamar el estilo moderno de C++. La clase funciona de cualquier manera, por lo que hay mucho beneficio práctico que puedes obtener al preocuparte por la diferencia.

2

Varios pensamientos:

  • Es agradable cuando no miembros trabajan a través de la API pública de la clase, ya que reduce la cantidad de código que:
    • necesario vigilar cuidadosamente para asegurar invariantes de clase,
    • debe cambiarse si se rediseña la implementación del objeto.
  • Cuando eso no es suficiente, un no miembro aún se puede hacer un friend.
  • Escribir una función no miembro es por lo general una pizca menos conveniente, como miembros no son implícitamente en su alcance, pero si se tiene en cuenta la evolución del programa:
    • Una vez que existe una función que no sea miembro y se comprende que la misma la funcionalidad sería útil para otros tipos, en general es muy fácil convertir la función en una plantilla y tenerla disponible no solo para ambos tipos, sino también para futuros tipos arbitrarios. Dicho de otra manera, las plantillas que no son miembros permiten una reutilización de algoritmo aún más flexible que el polimorfismo en tiempo de ejecución/despacho virtual: las plantillas permiten algo conocido como duck typing.
    • Un tipo existente con una función de miembro útil anima cortar y pegar a los otros tipos que deseen un comportamiento análogo porque la mayoría de las formas de convertir la función para su reutilización requieren que cada acceso implícito de miembro se haga un acceso explícito en un objeto particular, que va a ser una más tedioso de 30 segundos para el programador ....
  • funciones miembros permiten la notación object.function(x, y, z), que en mi humilde opinión es muy conveniente, expresiva e intuitiva. También funcionan mejor con funciones de descubrimiento/finalización en muchos IDE.
  • Una separación como funciones miembro y no miembro puede ayudar a comunicar la naturaleza esencial de la clase, sus invariantes y operaciones fundamentales, y agrupar de forma lógica las funciones complementarias y posiblemente ad-hoc de "conveniencia". Considere la sabiduría de Tony Hoare:

    "Hay dos formas de construir un diseño de software: una forma es hacerlo tan simple que obviamente no hay deficiencias, y la otra es hacerlo tan complicado que no haya deficiencias obvias. El primer método es mucho más difícil."

    • Aquí, el uso no sea miembro no es necesariamente mucho más difícil, pero usted tiene que pensar más acerca de cómo se está accediendo a datos de los miembros y métodos privados/protegidas y por qué, y qué operaciones son fundamentales. Tal búsqueda del alma mejoraría el diseño con funciones de miembros también, es más fácil ser perezoso acerca de: - /.
  • Como funcionalidad no miembros se expande en la sofisticación o recoge dependencias adicionales, las funciones se pueden mover en las cabeceras separadas y los archivos de implementación, incluso bibliotecas, para que los usuarios de la funcionalidad central única "pagan" para el uso de las piezas ellos quieren.

(respuesta de de todo género es una lectura obligada, tres veces si es nuevo para usted.)

Cuestiones relacionadas