¿Por qué ponemos funciones de miembros privados en los encabezados?

16

La respuesta a por qué colocamos variables miembro privadas en los encabezados de C ++ es que el tamaño de la clase debe conocerse en los puntos donde se declaran las instancias para que el compilador pueda generar código que se mueva apropiadamente sobre la pila.

¿Por qué necesitamos poner miembros privados en encabezados?

Pero, ¿hay alguna razón para declarar funciones privadas en la definición de clase?

La alternativa sería esencialmente el idioma de pimpl pero sin la indirección superflua.

¿Es esta característica del lenguaje más que un error histórico?

Praxeolítico
fuente

Respuestas:

11

Las funciones de miembros privados pueden ser virtual, y en implementaciones comunes de C ++ (que usan una tabla v), todos los clientes de la clase deben conocer el orden específico y el número de funciones virtuales. Esto se aplica incluso si una o más de las funciones de miembro virtual es private.

Puede parecer que esto es como "poner el carro antes que el caballo", porque las opciones de implementación del compilador no deberían afectar la especificación del lenguaje. Sin embargo, en realidad, el lenguaje C ++ en sí se desarrolló al mismo tiempo que una implementación funcional ( Cfront ), que utilizaba vtables.

Greg Hewgill
fuente
44
"las opciones de implementación no deberían afectar el lenguaje" es casi exactamente lo contrario del mantra C ++. La especificación no exige ninguna implementación en particular, pero hay muchas reglas específicamente diseñadas para cumplir con los requisitos de una elección de implementación particularmente eficiente.
Ben Voigt
Esto suena muy plausible. ¿Hay una fuente en esto?
Praxeolítico
@Praxeolitic: puede leer sobre la tabla de métodos virtuales .
Greg Hewgill
1
@Praxeolitic: encuentre una copia antigua del 'Manual de referencia de C ++ anotado' ( stroustrup.com/arm.html ): los autores hablan sobre varios detalles de implementación y cómo dio forma al lenguaje.
Michael Kohne
No estoy seguro de que esto realmente responda la pregunta. Para mí, solo aborda una faceta específica, aunque bien, y deja atrás el resto: ¿Por qué necesitamos poner no virtual funciones de miembros privados en los encabezados? Claro, solo por consistencia / simplicidad es un argumento, pero idealmente también tendríamos una lógica mecanicista y / o filosófica. Entonces, agregué una respuesta que creo que explica esto como una elección de diseño muy deliberada con efectos muy beneficiosos.
underscore_d
13

Si permitiste agregar métodos a una clase fuera de su definición, cualquiera podría agregarlos en cualquier lugar , en cualquier archivo.

Eso daría de inmediato a todos los códigos de clientes acceso trivial a miembros de datos privados y protegidos.

Una vez que haya terminado la definición de la clase, no hay forma de marcar algunos archivos como especialmente bendecidos por el autor para extenderla, solo hay unidades de traducción planas. Entonces, la única forma razonable de decirle al compilador que un conjunto particular de métodos son oficiales, o bendecidos por el autor de la clase, es declarándolos dentro de la clase.


Tenga en cuenta que tenemos acceso directo a la memoria en C ++, lo que significa que generalmente es trivial crear un tipo de sombra con el mismo diseño de memoria que su clase, agregar mis propios métodos (o simplemente hacer públicos todos los datos) y reinterpret_cast. O puedo encontrar el código de su función privada, o desarmarlo. O busque la dirección de la función en la tabla de símbolos y llame o directamente.

Estos especificadores de acceso no intentan evitar estos ataques, porque eso no es posible. Solo indican cómo se supone que se debe usar una clase.

Inútil
fuente
1
La definición de clase podría referirse a una entidad contenedor de funciones oculta del código del cliente. Alternativamente, esto podría invertirse y una definición de clase tradicional completa oculta del código del cliente podría designar una interfaz visible que solo describa a los miembros públicos y pueda revelar su tamaño.
Praxeolítico
1
Claro, pero el modelo de compilación no proporciona una forma razonable de detener el código del cliente que interpone su propia versión del contenedor oculto o una interfaz visible diferente con accesores adicionales.
Inútil
Ese es un buen punto, pero ¿no es cierto para las definiciones de todas las funciones miembro que no están en el encabezado de todos modos?
Praxeolítico
1
Sin embargo , todas las funciones miembro se declaran dentro de la clase. El punto es que no puede agregar nuevas funciones miembro que al menos no se declararon en la definición de clase (y la regla de una definición evita múltiples definiciones).
Inútil
Entonces, ¿qué pasa con una regla de un contenedor de funciones privadas?
Praxeolítico
8

La respuesta aceptada explica esto para las funciones privadas virtuales , pero eso solo responde a una faceta específica de la pregunta, que es considerablemente más limitada de lo que preguntó el OP. Entonces, necesitamos reformular: ¿Por qué estamos obligados a declarar funciones privadas no virtuales en los encabezados?

Otra respuesta invoca el hecho de que las clases deben declararse en un bloque, después de lo cual están selladas y no se pueden agregar. Eso es lo que estaría haciendo al omitir declarar un método privado en el encabezado y luego tratar de definirlo en otro lugar. Buen punto ¿Por qué algunos usuarios de la clase deberían poder aumentarlo de una manera que otros usuarios no puedan observar? Los métodos privados son parte de esto y no están excluidos de esto. Pero luego preguntas por qué están incluidos, y parece un poco tautológico. ¿Por qué los usuarios de clase deben saber sobre ellos? Si no fueran visibles, los usuarios no podrían agregar ninguno, y bueno, listo.

Entonces, quería proporcionar una respuesta que, en lugar de incluir métodos privados por defecto, proporcione puntos específicos a favor de que sean visibles para los usuarios. Se da una razón mecanicista para las funciones privadas no virtuales que requieren declaración pública en GotW # 100 de Herb Sutter sobre el lenguaje Pimpl como parte de su justificación. No hablaré sobre Pimpl aquí, ya que estoy seguro de que todos lo sabemos. Pero aquí está el bit relevante:

En C ++, cuando algo en una definición de clase de archivo de encabezado cambia, todos los usuarios de esa clase deben volver a compilarse, incluso si el único cambio fue a los miembros de la clase privada a los que los usuarios de la clase ni siquiera pueden acceder. Esto se debe a que el modelo de compilación de C ++ se basa en la inclusión textual, y porque C ++ supone que las personas que llaman conocen dos cosas principales sobre una clase que pueden ser afectadas por miembros privados:

  • Tamaño y diseño : [de miembros y funciones virtuales: se explica por sí mismo y es excelente para el rendimiento, pero no por qué estamos aquí]
  • Funciones : el código de llamada debe poder resolver las llamadas a las funciones miembro de la clase, incluidas las funciones privadas inaccesibles que se sobrecargan con funciones no privadas; si la función privada coincide mejor, el código de llamada no se compilará. (C ++ tomó la decisión deliberada de diseño de realizar una resolución de sobrecarga antes de verificar la accesibilidad por razones de seguridad. Por ejemplo, se consideró que cambiar la accesibilidad de una función de privado a público no debería cambiar el significado del código de llamada legal).

Sutter es, por supuesto, una fuente extremadamente confiable como miembro del Comité, por lo que conoce "una decisión de diseño deliberada" cuando la ve. Y la idea de exigir la declaración pública de métodos privados como una forma de evitar una semántica alterada o accesibilidad accidentalmente interrumpida más tarde es probablemente la razón más convincente. Afortunadamente, ¡ya que todo parecía inútil antes de ahora!

subrayado_d
fuente
" Pero luego preguntas por qué, y esa respuesta parece tautológica. Nuevamente, ¿por qué los usuarios de la clase necesitan saber sobre ellos? " No, no es tautológica. Los usuarios no necesariamente "necesitan saber sobre ellos". Lo que debe suceder es que debes evitar que las personas amplíen las clases sin el consentimiento del escritor de la clase. Por lo tanto, todos esos miembros deben declararse por adelantado. Que esto haga que los usuarios sepan sobre miembros privados es simplemente una consecuencia necesaria.
Nicol Bolas
1
@NicolBolas seguro. Probablemente no fue una muy buena redacción de mi parte. Quise decir que la respuesta solo explica la visibilidad de los métodos privados como consecuencia de una regla (muy válida) que cubre muchas cosas, en lugar de proporcionar una justificación sobre la visibilidad de los métodos privados específicamente. Por supuesto, la verdadera respuesta es una combinación de Useless, gnasher y mine. Solo quiero proporcionar otra perspectiva que aún no estaba aquí.
underscore_d
4

hay dos razones para hacer esto.

Primero, tenga en cuenta que el especificador de acceso es para el compilador y no es relevante en tiempo de ejecución. Acceder a un miembro privado fuera del alcance es un error de compilación .

Concisión

Considere una función que es corta, una o dos líneas. Existe para reducir la replicación de código en otros lugares, lo que también tiene la ventaja de poder cambiar cómo funciona un algoritmo o cualquier otra cosa en un lugar en lugar de muchos (por ejemplo, cambiar un algoritmo de clasificación).

¿Prefieres tener una o dos líneas rápidas en el encabezado, o tener el prototipo de la función allí más una implementación en alguna parte? Es más fácil de encontrar en el encabezado, y para funciones cortas, es mucho más detallado tener una implementación separada.

Hay otra gran ventaja, que es ...

Funciones en linea

Una función privada puede estar en línea, y esto necesariamente requiere que esté en el encabezado. Considera esto:

class A {
  private:
    inline void myPrivateFunction() {
      ...
    }

  public:
    inline void somePublicFunction() {
      myPrivateFunction();
      ...
    }
};

La función privada puede ser incorporada junto con la función pública. Eso se hace a discreción del compilador, ya que la inlinepalabra clave es técnicamente una sugerencia , no un requisito.

Comunidad
fuente
Todas las funciones definidas dentro del cuerpo de la clase se marcan automáticamente inline, no hay razón para usar la palabra clave allí.
Ben Voigt
@BenVoigt así es. No me di cuenta de eso, y he estado usando C ++ a tiempo parcial durante bastante tiempo. Ese lenguaje nunca deja de sorprenderme con pequeñas pepitas como esa.
No creo que un método deba estar en el encabezado para estar en línea. Solo necesita estar en la misma unidad de compilación. ¿Hay algún caso en el que tenga sentido tener unidades de compilación separadas para una clase?
Samuel Danielson
@SamuelDanielson correcto, pero una función privada debe estar en la definición de clase. La noción misma de "privado" implica que es parte de la clase. Sería posible tener una función no .cppmiembro en el archivo que está en línea por funciones miembro que están definidas fuera de la definición de clase, pero dicha función no sería privada.
El cuerpo de la pregunta era "¿hay alguna razón para declarar funciones privadas en la definición de clase [sic, por lo que el contexto muestra que OP realmente significaba declaración de clase]". Estás hablando de definir las funciones privadas. Esto no responde la pregunta. @SamuelDanielson Hoy en día, LTO significa que las funciones pueden estar en cualquier parte de un proyecto y mantener la misma posibilidad de estar en línea. En cuanto a dividir una clase en varias unidades de traducción, el caso más simple es que la clase es simplemente grande y desea dividirla semánticamente en múltiples archivos de origen. Estoy seguro de que se desalientan clases tan grandes, pero de todos modos
underscore_d
2

Otra razón para tener métodos privados en el archivo de encabezado: hay casos en los que un método en línea público no hace mucho más que llamar a uno o varios métodos privados. Tener los métodos privados en el encabezado significa que una llamada al método público puede estar completamente en línea con el código real de los métodos privados, y la alineación no se detiene con una llamada al método privado. Incluso desde una unidad de compilación diferente (y los métodos públicos generalmente se llamarían desde diferentes unidades de compilación).

Por supuesto, también existe la razón de que el compilador no puede detectar problemas con la resolución de sobrecarga si no conoce todos los métodos, incluidos los privados.

gnasher729
fuente
Excelente punto sobre la línea! Me imagino que esto es especialmente relevante para stdlib y otras bibliotecas con métodos definidos en línea. (si no tanto para el código interno, donde LTO puede hacer casi cualquier cosa a través de los límites de TU)
subrayado_d
0

Es para permitir que esas funciones accedan a los miembros privados. De lo contrario, los necesitaría frienden el encabezado de todos modos.

Si alguna función pudiera acceder a los miembros privados de la clase, privado sería inútil.

monstruo de trinquete
fuente
1
Tal vez debería haber sido más explícito en la pregunta: quise decir ¿por qué el lenguaje está diseñado de esta manera? ¿Por qué debe serlo o por qué este diseño es bueno?
Praxeolítico