2009-06-28 19 views
12

Herb Sutter ha dicho que la forma más orientada a objetos para escribir métodos en C++ es el uso de funciones no miembro de no amigos. ¿Debería eso significar que debería tomar métodos privados y convertirlos en funciones de no amigos no miembros? Cualquier variable miembro que estos métodos puedan necesitar se puede pasar como parámetros.Funciones no miembro no miembro frente a funciones privadas

Ejemplo (antes):

class Number { 
public: 
    Number(int nNumber) : m_nNumber(nNumber) {} 
    int CalculateDifference(int nNumber) { return minus(nNumber); } 
private: 
    int minus(int nNumber) { return m_nNumber - nNumber; } 
    int m_nNumber; 
}; 

Ejemplo (después):

int minus(int nLhsNumber, int nRhsNumber) { return nLhsNumber - nRhsNumber; } 
class Number { 
public: 
    Number(int nNumber) : m_nNumber(nNumber) {} 
    int CalculateDifference(int nNumber) { return minus(m_nNumber, nNumber); } 
private: 
    int m_nNumber; 
}; 

estoy en el camino correcto? ¿Deben todos los métodos privados moverse a las funciones de no amigos no miembros? ¿Cuáles deberían ser las reglas que te dirían lo contrario?

+0

"¿Debería significar que debería tomar métodos privados y convertirlos en funciones de no amigos no miembros?" él está hablando de funciones públicas. es decir, la interfaz, no los detalles de implementación, como dijo la respuesta de Daniel, antes de que se lo quitara (¿por qué?) –

+1

Este artículo de scott meyers lo explica en profundidad: http://www.ddj.com/cpp/184401197 –

Respuesta

16

Creo en las funciones gratuitas y estoy de acuerdo con Sutter, pero mi entendimiento es en la dirección opuesta. No es que deba hacer que sus métodos públicos dependan de funciones gratuitas en lugar de métodos privados, sino que puede construir una interfaz más rica fuera de la clase con funciones gratuitas mediante el uso de la interfaz pública proporcionada.

Es decir, no empuja sus partes privadas fuera de la clase, sino que reduce la interfaz pública al mínimo que le permite construir el resto de la funcionalidad con el menor acoplamiento posible: solo utilizando la interfaz pública.

En su ejemplo, lo que movería fuera de la clase es el método CalculateDifference si se puede representar efectivamente en términos de otras operaciones.

class Number { // small simple interface: accessor to constant data, constructor 
public: 
    explicit Number(int nNumber) : m_nNumber(nNumber) {} 
    int value() const { return m_nNumber; } 
private: 
    int m_nNumber; 
}; 
Number operator+(Number const & lhs, Number const & rhs) // Add addition to the interface 
{ 
    return Number(lhs.value() + rhs.value()); 
} 
Number operator-(Number const & lhs, Number const & rhs) // Add subtraction to the interface 
{ 
    return Number(lhs.value() - rhs.value()); 
} 

La ventaja es que si decide volver a definir sus componentes internos numéricos (no hay mucho que se puede hacer con una clase tan simple), siempre y cuando se mantenga su interfaz pública constante, entonces todas las demás funciones se trabajar fuera de la caja. Los detalles internos de implementación no lo forzarán a redefinir todos los otros métodos.

La parte difícil (no en el ejemplo simplista anterior) es determinar cuál es la menor interfaz que debe proporcionar. El artículo (GotW#84), al que se hace referencia a partir de una pregunta anterior, es un excelente ejemplo. Si lo lee en detalle, encontrará que puede reducir en gran medida la cantidad de métodos en std :: basic_string, manteniendo la misma funcionalidad y rendimiento. El conteo bajaría de 103 funciones miembro a solo 32 miembros. Eso significa que los cambios de implementación en la clase afectarán solo a 32 en lugar de a 103 miembros y, a medida que se mantenga la interfaz, las 71 funciones gratuitas que pueden implementar el resto de la funcionalidad en términos de los 32 miembros no tendrán que cambiarse.

Ese es el punto importante: está más encapsulado ya que limita el impacto de los cambios de implementación en el código.

Al salir de la pregunta original, aquí hay un ejemplo simple de cómo el uso de funciones gratuitas mejora la ubicación de los cambios en la clase. Supongamos una clase compleja con una operación de adición realmente compleja.Se podía ir a por ello y poner en práctica todas las anulaciones de operador como funciones miembro, o simplemente puede poner en práctica con la misma facilidad y eficacia sólo algunos de ellos internamente y proporcionar el resto como funciones gratuitas:

class ReallyComplex 
{ 
public: 
    ReallyComplex& operator+=(ReallyComplex const & rhs); 
}; 
ReallyComplex operator+(ReallyComplex const & lhs, ReallyComplex const & rhs) 
{ 
    ReallyComplex tmp(lhs); 
    tmp += rhs; 
    return tmp; 
} 

Se puede ver fácilmente que ningún importa cómo el original operator+= realiza su tarea, el operator+ libre realiza su deber correctamente. Ahora, con todos y cada uno de los cambios a la clase, se deberá actualizar operator+=, pero el operator+ externo permanecerá intacto por el resto de su vida útil.

El código anterior es un patrón común, mientras que por lo general en lugar de recibir la lhs operando por referencia constante y la creación de un objeto temporal dentro, que puede ser cambiado para que el parámetro es en sí mismo una copia valor, ayudando al compilador con algunas optimizaciones :

ReallyComplex operator+(ReallyComplex lhs, ReallyComplex const & rhs) 
{ 
    lhs += rhs; 
    return lhs; 
} 
+0

+1. @David, tengo una pregunta: ¿Haría alguna diferencia si en lugar de la primera, el segundo parámetro es una copia de valor (en su último ejemplo)? – Nawaz

11

No todos los métodos privados se deben mover a la función non-member non-friend, pero los que no necesitan acceso a sus datos privados deberían ser. Debe dar acceso a la menor función posible, para encapsular sus clases lo más posible.

Recomiendo leer Effective C++ from Scott Meyers que explican por qué debería hacer esto y cuándo es apropiado.

Editar: Me gustaría añadir que esto es menos cierto para el método privado que para los públicos, aunque sigue siendo válido. Como la encapsulación es proporcional a la cantidad de código que rompería modificando su método, tiene una función de miembro privada, aunque no requiere acceso a los miembros de datos. Eso es porque modificar ese código rompería el código pequeño y solo el código sobre el que tienes control.

0

Depende en gran medida de su diseño y la situación en que se encuentre. Cuando escribo mi código, realmente solo pongo las funciones public y protected en clases. El resto es detalles de implementación que forman parte del archivo .cpp.

A menudo utilizo la expresión pImpl también. En mi opinión, ambos enfoques tienen los siguientes beneficios:

  • Interfaz limpia diseño

    Los usuarios que se ocupan de su interfaz será mejor entenderlo sin ahondar en los detalles de implementación si no tienen que hacerlo.

  • desacoplamiento de la aplicación

    Si cambia algo en un archivo .cpp sin cambiar la interfaz, sólo tendrá que volver a compilar un archivo .cpp (unidad de compilación) y re-enlace de la aplicación, lo que resulta en Tiempos de compilación mucho más rápidos.

  • Ocultar dependencias de otras clases, que utilizan la interfaz de

    Si todo se pone en el archivo de cabecera, a veces hay que incluir otras cabeceras, que son "parte de su interfaz" Y a otros incluirlos sin importa si ellos no quieren. Poner implementaciones reales en las unidades de compilación ocultará estas dependencias.

Pero a veces se escribe bibliotecas o implementaciones sólo de encabezado, por lo tanto, no se puede poner las cosas en unidades de compilación, ya que este enfoque requeriría no sólo la inclusión de su liberación, pero que une en contra de su lib también.

2

Parece que esta pregunta ya ha sido addressed al responder una pregunta diferente.

Tales reglas tienden a ser buenos servidores y malos maestros, hay intercambios involucrados. ¿El resultado es más sostenible si aplicas la transformación que sugieres? ¿Por qué? Creo que el beneficio previsto es que al reducir el número de métodos que tratan directamente con los datos privados del objeto, puede comprender más fácilmente su comportamiento y, por lo tanto, hacer que sea más fácil de mantener.

No creo que su ejemplo posterior logre este objetivo. [Puede notar que el ejemplo "después" de arriba no compilará de todos modos, pero esa es otra historia.] Si lo ajustamos para implementar las funciones externas puramente en términos de métodos públicos en lugar de estado interno (agregue un descriptor de acceso para el valor) entonces supongo que hemos tenido alguna ganancia. ¿Es suficiente para garantizar el trabajo? Mi opinión: no. Creo que los beneficios del movimiento propuesto para la función externa se vuelven mucho mayores cuando los métodos actualizan los valores de los datos en el objeto. Me gustaría implementar un pequeño número de mutadores que mantienen los invasores e implementar los principales métodos de "negocios" en términos de esos, externalizar esos métodos es una forma de garantizar que solo puedan funcionar en términos de mutadores.

+0

La pregunta usted señala que está relacionado, pero no es lo mismo. La pregunta original trata sobre los IDE y cómo ayudan al programador, no con el problema en sí. Entonces, el 'bien contestado' es bastante subjetivo cuando se trata de una respuesta con tan solo 2 votos al alza. Esa respuesta no está tan bien considerada. –

+0

La pregunta no es la misma, pero la respuesta allí aborda esta. Ajusté la redacción y reduje la subjetividad. – djna

Cuestiones relacionadas