Cómo las funciones no miembro mejoran la encapsulación

Leí el artículo de Scott Meyers sobre el tema y bastante confundido acerca de lo que él está hablando. Tengo 3 preguntas aquí.

Pregunta 1

Para explicarlo en detalle, supongamos que estoy escribiendo una clase vector simple vector con métodos como push_back , insert y operator [] . Si siguiera el algoritmo de Meyers, terminaría con todas las funciones de amigos que no son miembros. Tendré una clase de vector con pocos miembros privados y muchas funciones de amigos que no son miembros. ¿De esto es de lo que está hablando?

Pregunta 2

Todavía no entiendo cómo las funciones no miembro mejoran la encapsulación. Considere el código dado en el artículo de Meyers.

 class Point { public: int getXValue() const; int getYValue() const; void setXValue(int newXValue); void setYValue(int newYValue); private: ... // whatever... }; 

Si se sigue su algoritmo, los métodos setXXXX deberían ser no miembros. Mi pregunta es ¿cómo aumenta la encapsulación? Él también dice

Ahora hemos visto que una forma razonable de medir la cantidad de encapsulamiento en una clase es contar la cantidad de funciones que podrían romperse si cambia la implementación de la clase.

Hasta que no mantengamos la firma del método intacta cuando la implementación de clase cambie, ningún código de cliente se va a romper y está bien encapsulado, ¿no? Lo mismo se aplica para las funciones que no son miembros también. Entonces, ¿cuál es la ventaja que proporciona la función no miembro?

Pregunta 3

Citando su algoritmo

 else if (f needs type conversions on its left-most argument) { make fa non-member function; if (f needs access to non-public members of C) make fa friend of C; } 

¿A qué se refería con f necesita conversiones de tipo en su argumento más a la izquierda ? Él también dice lo siguiente en el artículo.

Además, ahora vemos que la afirmación común de que “las funciones de amigo violan la encapsulación” no es del todo cierto. Los amigos no violan la encapsulación, solo la disminuyen, exactamente de la misma manera que un miembro.

Este y el algoritmo anterior son contradictorios, ¿verdad?

Pregunta 1

En este caso, seguir el algoritmo de Meyers te dará funciones de miembro:

  • ¿Necesitan ser virtuales? No.
  • ¿Son operator<< u operator>> ? No.
  • ¿Necesitan conversiones de tipo? No.
  • ¿Pueden implementarse en términos de la interfaz pública? No.
  • Entonces hazlos miembros.

Su consejo es hacerles amigos solo cuando realmente necesitan serlo; para favorecer a los no amigos que no son miembros, más que a los amigos.

Pregunta 2

Las funciones SetXXXX necesitan acceder a la representación interna (privada) de la clase, por lo que no pueden ser no miembros que no sean amigos; entonces, argumenta Meyers, deberían ser miembros en lugar de amigos.

La encapsulación se produce al ocultar los detalles de cómo se implementa la clase; usted define una interfaz pública por separado de una implementación privada. Si luego inventa una mejor implementación, puede cambiarla sin cambiar la interfaz pública, y cualquier código que use la clase continuará funcionando. Así que el "número de funciones que podría romperse" de Meyers cuenta las funciones de miembro y amigo (que podemos rastrear fácilmente mirando la definición de clase), pero no cualquier función no miembro no miembro que use la clase a través de su interfaz pública.

Pregunta 3

Esto ha sido respondido .

Los puntos importantes que debe alejarse del consejo de Meyers son:

  • Las clases de diseño tienen interfaces públicas limpias y estables separadas de su implementación privada;
  • Solo haga miembros de funciones o amigos cuando realmente necesiten acceder a la implementación;
  • Solo haga amigos a las funciones cuando no puedan ser miembros.

El significado f necesita conversiones de tipo en la arg de la izquierda es la siguiente:

considerar seguir a senario:

 Class Integer { private: int num; public: int getNum( return num;) Integer(int n = 0) { num = n;} Integer(const Integer &rhs)) { num = rhs.num ;} Integer operator * (const Integer &rhs) { return Integer(num * rhs.num); } } int main() { Integer i1(5); Integer i3 = i1 * 3; // valid Integer i3 = 3 * i1 ; // error } 

En el código anterior i3 = i1 * 3 es equivalente a this->operator*(3) que es válido ya que 3 se convierte implícitamente en Integer.

Donde como en el último i3 = 3 * i1 es equivalente a 3.operator*(i1) , según la regla cuando el operador de sobrecarga usa la función miembro, el objeto invocado debe ser de la misma clase. pero aquí no es eso.

Para hacer que Integer i3 = 3 * i1 funcione, uno puede definir la función no miembro de la siguiente manera:

 Integer operator * (const Integer &lhs , const Integer &rhs) // non-member function { return Integer(lhs.getNum() * rhs.getNum()); } 

Creo que obtendrás idea de este ejemplo …..

De los cuatro casos que proporciona para realizar funciones que no son miembros, lo más parecido que obtendrían los métodos vector propuestos es este:

 else if (f can be implemented via C's public interface) make fa non-member function; 

Pero no puede implementar métodos como push_back , insert u operator[] través de una interfaz pública. Esas son la interfaz pública. Podría ser posible implementar push_back en términos de insert , pero en gran medida, ¿qué interfaz pública vas a utilizar para tales métodos?

Además, los casos de amistad con las funciones no miembro son casos realmente especiales, como yo lo veo, el operator<< y el operator>> , y las conversiones de tipo, requerirían datos muy precisos y no filtrados de la clase. Estos métodos son naturalmente muy invasivos.

Si bien no soy fanático del Dr. Dobbs, ni de ninguno de los "gurús de C ++" alegados, creo que en este caso podría estar adivinando su propia implementación. El algoritmo de Scott Meyer me parece razonable.

Eche un buen vistazo a los algoritmos STL. sort , copy , transform , etc. operar en iteradores y no son funciones de miembros.

También estás equivocado sobre su algoritmo. Las funciones de establecer y obtener no se pueden implementar con la interfaz pública de Point.

Pregunta 2

Scott Meyers también sugirió lo siguiente si lo recuerda:

-> Mantenga la interfaz de clase completa y mínima.

Ver el siguiente escenario:

 class Person { private: string name; unsigned int age; long salary; public: void setName(string);// assme the implementation void setAge(unsigned int); // assme the implementation void setSalary(long sal); // assme the implementation void setPersonData() { setName("Scott"); setAge(25); selSalary(50000); } } 

aquí setPersonData() es una función miembro pero en última instancia, lo que hace también se puede lograr haciendo que la función no miembro funcione así y mantendrá la interfaz de clase mínima y no inflará la clase con suficiente función miembro innecesariamente.

  void setPersonData(Person &p) { p.setName("Scott"); p.setAge(25); p.selSalary(50000); } 

Supongo que el punto general es que es beneficioso implementar siempre cosas en términos de otras cosas si es posible. Al implementar la funcionalidad como funciones sin amigos, se garantiza que esta funcionalidad no se rompa si cambia la representación de la clase.

En la vida real, supongo que podría tener un problema: es posible que pueda implementar algo en términos de la interfaz pública con la implementación actual , pero si hay cambios en la clase, esto podría no ser posible más (y usted Tendré que empezar a declarar cosas como amigos). (Por ejemplo, cuando se trata de optimización algorítmica, la función gratuita puede beneficiarse de algunos datos en caché adicionales, que no deben exponerse al público).

Entonces, la directriz que obtendré de ella: use el sentido común, pero no le tenga miedo a las funciones gratuitas. No hacen que su código C ++ esté menos orientado a objetos.


Otra cosa es, probablemente, una interfaz que consiste completamente en getters y setters. Esto apenas encapsula nada.

En el caso de Point en particular, puede tener la tentación de almacenar los datos como int coords[2] lugar, y en este sentido los getters y setters pueden tener un significado (pero también se puede considerar siempre la facilidad de uso frente a la facilidad de implementación).

Pero si pasas a clases más complicadas, deberían hacer algo (alguna funcionalidad central) que no sea simplemente dar acceso a sus datos.


Cuando se trata de vector, algunos de sus métodos podrían haber sido funciones libres: assign (en términos de clear + insert), at, back, front (en términos de tamaño + operator[] ), empty (en términos de tamaño o comenzar / end), pop_back (borrar + tamaño), push_back (insertar + tamaño), finalizar (comenzar + tamaño), reiniciar y deshacer (comenzar y finalizar).

Pero si se toma rigurosamente, esto podría conducir a interfaces bastante confusas, por ejemplo

  for (vector::iterator it = v.begin(); it != end(v); ++it) 

Además, aquí uno debería considerar las capacidades de otros contenedores. Si std :: list no puede implementar end como una función libre, std :: vector tampoco debería (las plantillas necesitan un patrón uniforme para iterar sobre un contenedor).

De nuevo, usa el sentido común.

Específicamente dice “funciones de no amigos que no son miembros” (énfasis mío). Si necesita hacer que la función no miembro sea un demonio, sus algoritmos dicen que debe ser una función miembro a menos que sea operador >> o operador << o necesite conversiones de tipo en su argumento más a la izquierda.

Hasta que no mantengamos la firma del método intacta cuando la implementación de clase cambie, ningún código de cliente se va a romper y está bien encapsulado, ¿no? Lo mismo se aplica para las funciones que no son miembros también. Entonces, ¿cuál es la ventaja que proporciona la función no miembro?

Meyers dice que una clase con muchos métodos está menos encapsulada que una clase con menos métodos, porque las implementaciones de todos esos métodos internos están sujetas a cambios. Si alguno de los métodos podría haber sido no miembro, eso reduciría la cantidad de métodos que podrían verse afectados por los cambios internos de la clase.

¿A qué se refería con f necesita conversiones de tipo en su argumento más a la izquierda?

Creo que se está refiriendo a operadores, funciones que tendrían un argumento implícito más a la izquierda si fueran funciones miembro.