una resueltos que programacion orientada objetos miembros herencia ejercicios ejemplos constructores codigo clases clase c++ stl encapsulation

resueltos - programacion orientada a objetos ejemplos c++



¿Por qué std:: pair expone las variables miembro? (7)

¡Se podría argumentar que std::pair sería mejor tener funciones de acceso para acceder a sus miembros! Notablemente para casos degenerados de std::pair podría haber una ventaja. Por ejemplo, cuando al menos uno de los tipos es una clase vacía, no final, los objetos podrían ser más pequeños (la base vacía podría convertirse en una base que no necesitaría obtener su propia dirección).

En el momento en que se inventó std::pair estos casos especiales no se consideraron (y no estoy seguro de si la optimización de la base vacía estaba permitida en el borrador del documento de trabajo en ese momento). Sin embargo, desde un punto de vista semántico no hay muchas razones para tener funciones de acceso: claramente, los accesores necesitarían devolver una referencia mutable para objetos no const . Como resultado, el descriptor de acceso no proporciona ninguna forma de encapsulación.

Por otro lado, hace que sea [ligeramente] más difícil para el optimizador ver qué sucede cuando se utilizan funciones de acceso, por ejemplo, porque se introducen puntos de secuencia adicionales. Me imagino que Meng Lee y Alexander Stepanov incluso midieron si hay una diferencia (yo tampoco). Incluso si no lo hicieran, proporcionar acceso a los miembros directamente no es más lento que pasar por una función de acceso, mientras que lo contrario no es necesariamente cierto.

No formé parte de la decisión y el estándar C ++ no tiene una justificación, pero supongo que fue una decisión deliberada hacer que los miembros sean miembros de datos públicos.

Desde http://www.cplusplus.com/reference/utility/pair/ , sabemos que std::pair tiene dos variables miembro, la first y la second .

¿Por qué los diseñadores de STL decidieron exponer dos variables miembro, la first y la second , en lugar de ofrecer un getFirst() y un getSecond() ?


El propósito principal de getters y setters es obtener control sobre el acceso. Es decir, si expone "primero" como una variable, cualquier clase puede leerlo y escribirlo (si no es const ) sin decirle a la clase de la que forma parte. En varios casos, eso puede plantear serios problemas.

Por ejemplo, supongamos que tiene una clase que representa el número de pasajeros en un barco. Almacena el número de pasajeros como un número entero. Si expone ese número como una variable simple, sería posible que las funciones externas le escribieran. Eso podría dejarlo en un caso en el que en realidad hay 10 pasajeros, pero alguien cambió la variable (tal vez accidentalmente) a 50. Este es un caso para un captador del número de pasajeros (pero no un setter, que presentaría el mismo problema )

Un ejemplo para captadores y definidores sería una clase que representa un vector matemático en el que desea almacenar en caché cierta información sobre el vector. Digamos que quieres almacenar la longitud. En este caso, cambiar vec.x probablemente cambiaría la longitud / magnitud. Por lo tanto, no solo necesita hacer x envuelto en un getter, sino que debe proporcionar un setter para x, que sabe actualizar la longitud en caché del vector. (Por supuesto, la mayoría de las bibliotecas matemáticas reales no almacenan en caché estos valores y, por lo tanto, exponen las variables).

Entonces, la pregunta que debe hacerse en el contexto de su uso es: ¿esta clase posiblemente tendrá que controlar o ser alertado de los cambios en esta variable?

La respuesta en algo como std :: pair es un "no" plano. No hay ningún caso para controlar el acceso a los miembros de una clase cuyo único propósito es contener a esos miembros. Ciertamente, no es necesario que el par sepa si esas variables se han tocado, teniendo en cuenta que esos son sus dos únicos miembros y, por lo tanto, no tiene un estado que actualizar si cambia. pair ignora lo que realmente contiene y su significado, por lo que rastrear lo que contiene no vale la pena.

Dependiendo del compilador y de cómo está configurado, los captadores y establecedores pueden introducir una sobrecarga. Probablemente eso no sea importante en la mayoría de los casos, pero si los pusiera en algo fundamental como std::pair , sería una preocupación no trivial. Como tal, su adición necesitaría justificación, lo cual, como acabo de explicar, no puede ser.


La razón es que no es necesario imponer una invariante real en la estructura de datos, ya que std::pair modela un contenedor de propósito general para dos elementos. En otras palabras, se supone que un objeto de tipo std::pair<T, U> es válido para cualquier posible first y second elemento de tipo T y U , respectivamente. Del mismo modo, las mutaciones posteriores en el valor de sus elementos no pueden afectar realmente la validez de std::pair per se.

Alex Stepanov (el autor del STL) presenta explícitamente este principio de diseño general durante su curso Programación eficiente con componentes , al comentar sobre el contenedor singleton (es decir, un contenedor de un elemento).

Por lo tanto, aunque el principio en sí mismo puede ser una fuente de debate, esta es la razón detrás de la forma de std::pair .


Los captadores y establecedores son útiles si uno cree que la abstracción está garantizada para aislar a los usuarios de las elecciones de diseño y los cambios en esas elecciones, ahora o en el futuro.

El ejemplo típico de "ahora" es que el setter / getter podría tener lógica para validar y / o calcular el valor; por ejemplo, use un setter para un número de teléfono, en lugar de exponer directamente el campo, de modo que pueda verificar el formato; use un captador para una colección para que el captador pueda proporcionar una vista de solo lectura del valor del miembro (una colección) a la persona que llama.

El ejemplo canónico ( aunque malo ) para "cambios en el futuro" es Point : ¿debería exponer x y getX() y getY() ? La respuesta habitual es usar getters / setters porque en algún momento en el futuro es posible que desee cambiar la representación interna de cartesiana a polar y no desea que sus usuarios se vean afectados (o que dependan de esa decisión de diseño) .

En el caso de std::pair , la intención es que esta clase ahora y para siempre represente dos y exactamente dos valores (de tipo arbitrario) directamente, y proporcione sus valores a pedido. Eso es. Y es por eso que el diseño utiliza el acceso directo a los miembros, en lugar de pasar por un getter / setter.


Los captadores y establecedores suelen ser útiles si se piensa que obtener o establecer el valor requiere una lógica adicional (cambiar algún estado interno). Esto se puede agregar fácilmente al método. En este caso, std::pair solo se usa para proporcionar 2 valores de datos. Nada más y nada menos. Y por lo tanto, agregar la verbosidad de un getter y setter sería inútil.


Me sorprendió la cantidad de comentarios que no muestran una comprensión básica del diseño orientado a objetos (¿eso prueba que c ++ no es un lenguaje OO?). Sí, el diseño de std :: pair tiene algunos rasgos históricos, pero eso no hace que un mal diseño sea bueno; ni debe usarse como excusa para negar el hecho. Antes de despotricar sobre ello, déjame responder algunas de las preguntas en los comentarios:

¿No crees que int también debería tener un setter y getter

Sí, desde el punto de vista del diseño, deberíamos usar accesores porque al hacerlo no perdemos nada más que ganar flexibilidad adicional. Algunos algoritmos más nuevos pueden querer empaquetar bits adicionales en la clave / valores, y no puede codificarlos / decodificarlos sin accesores.

¿Por qué envolver algo en un getter si no hay lógica en el getter?

¿Cómo sabes que no habría lógica en el getter / setter? Un buen diseño no debe limitar la posibilidad de implementación basada en conjeturas. Debe ofrecer la mayor flexibilidad posible. Recuerde el diseño de std: pair también decide el diseño del iterador, y al exigir a los usuarios que accedan directamente a las variables miembro, el iterador debe devolver estructuras que realmente almacenen claves / valores juntos. Eso resulta ser una gran limitación. Hay algoritmos que necesitan mantenerlos separados. Hay algoritmos que no almacenan clave / valores explícitamente en absoluto. Ahora tienen que copiar los datos durante la iteración.

Contrariamente a la creencia popular, tener objetos que no hacen más que almacenar variables miembro con getters y setters no es "la forma en que se deben hacer las cosas"

Otra suposición salvaje.

OK, me detendría aquí.

Para responder a la pregunta original: std :: pair eligió exponer las variables miembro porque quien lo diseñó no reconoció ni priorizó la importancia de un contrato flexible. Obviamente, tenían una idea / visión muy limitada sobre cómo deberían implementarse los pares clave-valor en un mapa / tabla hash, y para empeorar la situación, dejaron que una visión tan limitada sobre la implementación se desbordara para comprometer el diseño. Por ejemplo, ¿qué sucede si quiero implementar un reemplazo de std: unordered_map que almacena la clave y los valores en matrices separadas basadas en un esquema de direccionamiento abierto con sondeo lineal? Esto puede aumentar en gran medida el rendimiento de la memoria caché para pares con claves pequeñas y valores grandes, ya que no necesita un salto largo en los espacios ocupados por los valores para sondear las claves. Si se hubieran elegido los accesos std :: pair, sería trivial escribir un iterador de estilo STL para esto. Pero ahora es simplemente imposible lograr esto sin obtener una copia de datos adicional.

Noté que también exigen el uso de hashing abierto (es decir, encadenamiento cerrado) para la implementación de std :: unordered_map. Esto no solo es extraño desde el punto de vista del diseño (¿por qué quiere restringir la forma en que se implementan las cosas?), Sino que también es bastante tonto en términos de implementación: las tablas hash encadenadas que usan la lista vinculada es quizás la más lenta de todas las categorías. Vaya a Google en la web, podemos encontrar fácilmente que std: unordered_map es a menudo el felpudo de un punto de referencia de tabla hash. Incluso tiende a ser más lento que el HashMap de Java (no sé cómo lograron retrasarse en este caso, ya que HashMap también es una tabla hash encadenada). Una vieja excusa es que la tabla hash encadenada tiende a funcionar mejor cuando el load_factor se acerca a 1, lo cual es totalmente inválido porque 1) hay muchas técnicas en el direccionamiento abierto de la familia para lidiar con este problema: alguna vez se supo de rayuela o robin-hood hashing, y este último ha estado allí por 30 años extraños; 2) una tabla hash encadenada agrega la sobrecarga de un puntero (un buen 8 bytes en máquinas de 64 bits) para cada entrada, por lo que cuando decimos que el load_factor de un unordered_map se acerca a 1, ¡no es 100% de uso de memoria! Deberíamos tener eso en cuenta y comparar el rendimiento de unordered_map con alternativas con el mismo uso de memoria. Y resulta que alternativas como Google Dense HashMap es 3-4 veces más rápido que std :: unordered_map.

¿Por qué son relevantes? Porque, curiosamente, exigir el hashing abierto hace que el diseño de std :: pair se vea menos mal, ahora que no necesitamos la flexibilidad de una estructura de almacenamiento alternativa. Además, la presencia de std :: pair hace que sea casi imposible adoptar algoritmos más nuevos / mejores para escribir un reemplazo directo de std :: unordered_map. A veces te preguntas si lo hicieron intencionalmente para que el pobre diseño de std :: pair y la implementación peatonal de std :: unordered_map puedan sobrevivir más tiempo juntos. Por supuesto que estoy bromeando, así que quienquiera que los haya escrito no se ofenda. De hecho, las personas que usan Java o Python (OK, admito que Python es una exageración) querrían agradecerles por hacerles sentir bien por ser "tan rápidos como C ++".


Para el std::pair original C ++ 03 std::pair , las funciones para acceder a los miembros no servirían para nada.

A partir de C ++ 11 y posterior (ahora estamos en C ++ 14, con C ++ 17 llegando rápidamente) std::pair es un caso especial de std::tuple , donde std::tuple puede tener cualquier número de items. Como tal, tiene sentido tener un captador parametrizado, ya que no sería práctico inventar y estandarizar un número arbitrario de nombres de elementos. Por lo tanto, puede usar std::get también para un std::pair .

Entonces, las razones para el diseño son históricas, que el std::pair actual es el resultado final de una evolución hacia una mayor generalidad.

En otras noticias:

respecto a

" Hasta donde yo sé, será mejor si encapsulamos dos variables miembro anteriores y le damos un getFirst(); y getSecond()

No, eso es basura.

Es como decir que un martillo siempre es mejor, ya sea que esté clavando, clavando con tornillos o recortando un pedazo de madera. Especialmente en el último caso, un martillo simplemente no es una herramienta útil. Los martillos pueden ser muy útiles, pero eso no significa que sean "mejores" en general: eso no tiene sentido.