Entonces noté que es posible evitar poner funciones privadas en los encabezados haciendo algo como esto:
// In file pred_list.h:
class PredicateList
{
int somePrivateField;
friend class PredicateList_HelperFunctions;
public:
bool match();
}
// In file pred_list.cpp:
class PredicateList_HelperFunctions
{
static bool fullMatch(PredicateList& p)
{
return p.somePrivateField == 5; // or whatever
}
}
bool PredicateList::match()
{
return PredicateList_HelperFunctions::fullMatch(*this);
}
La función privada nunca se declara en el encabezado, y los consumidores de la clase que importa el encabezado nunca necesitan saber que existe. Esto es necesario si la función auxiliar es una plantilla (la alternativa es poner el código completo en el encabezado), que es cómo "descubrí" esto. Otra ventaja de no tener que volver a compilar todos los archivos que incluyen el encabezado si agrega / elimina / modifica una función de miembro privado. Todas las funciones privadas están en el archivo .cpp.
Entonces...
- ¿Es este un patrón de diseño conocido para el que hay un nombre?
- Para mí (proveniente de un fondo de Java / C # y aprendiendo C ++ en mi propio tiempo), esto parece algo muy bueno, ya que el encabezado define una interfaz, mientras que .cpp define una implementación (y el tiempo de compilación mejorado es Un buen bono). Sin embargo, también huele como si estuviera abusando de una función de lenguaje que no está destinada a usarse de esa manera. Entonces, cual es? ¿Es esto algo que desaprobaría ver en un proyecto profesional de C ++?
- ¿Alguna trampa que no esté pensando?
Soy consciente de Pimpl, que es una forma mucho más robusta de ocultar la implementación en el borde de la biblioteca. Esto es más para usar con clases internas, donde Pimpl causaría problemas de rendimiento o no funcionaría porque la clase debe tratarse como un valor.
EDITAR 2: la excelente respuesta de Dragon Energy a continuación sugiere la siguiente solución, que no utiliza la friend
palabra clave en absoluto:
// In file pred_list.h:
class PredicateList
{
int somePrivateField;
class Private;
public:
bool match();
}
// In file pred_list.cpp:
class PredicateList::Private
{
public:
static bool fullMatch(PredicateList& p)
{
return p.somePrivateField == 5; // or whatever
}
}
bool PredicateList::match()
{
return PredicateList::Private::fullMatch(*this);
}
Esto evita el factor de choque de friend
(que parece haber sido demonizado goto
) al tiempo que mantiene el mismo principio de separación.
fuente
Respuestas:
Es un poco esotérico, por decir lo menos, como ya reconociste, lo que podría hacer que me rasque la cabeza por un momento cuando empiezo a encontrar tu código preguntándome qué estás haciendo y dónde se implementan estas clases auxiliares hasta que empiezo a elegir tu estilo. / hábitos (en ese momento podría acostumbrarme por completo).
Me gusta que estés reduciendo la cantidad de información en los encabezados. Especialmente en bases de código muy grandes, que pueden tener efectos prácticos para reducir las dependencias de tiempo de compilación y, en última instancia, los tiempos de compilación.
Sin embargo, mi reacción instintiva es que si siente la necesidad de ocultar los detalles de implementación de esta manera, para favorecer el paso de parámetros a funciones independientes con vinculación interna en el archivo fuente. Por lo general, puede implementar funciones de utilidad (o clases completas) útiles para implementar una clase en particular sin tener acceso a todos los componentes internos de la clase y, en su lugar, simplemente pasar las relevantes desde la implementación de un método a la función (o constructor). Y, naturalmente, eso tiene la ventaja de reducir el acoplamiento entre su clase y los "ayudantes". También tiene una tendencia a generalizar lo que de otro modo podrían haber sido "ayudantes" si descubres que están comenzando a cumplir un propósito más general aplicable a la implementación de más de una clase.
Si eso se vuelve difícil de manejar, consideraría una segunda solución más idiomática que es el pimpl (me doy cuenta de que mencionó problemas con él, pero creo que puede generalizar una solución para evitar aquellos con un esfuerzo mínimo). Eso puede mover una gran cantidad de información que su clase necesita ser implementada, incluidos sus datos privados fuera del encabezado al por mayor. Los problemas de rendimiento del pimpl pueden mitigarse en gran medida con un asignador de tiempo constante * barato como una lista gratuita, al tiempo que conserva la semántica de valor sin tener que implementar un copiador definido por el usuario completo.
Personalmente, solo después de agotar esas posibilidades consideraría algo como esto. Creo que es una idea decente si la alternativa es como métodos más privados expuestos al encabezado, y tal vez solo la naturaleza esotérica sea la preocupación práctica.
Una alternativa
Una alternativa que apareció en mi cabeza en este momento que cumple en gran medida tus mismos propósitos en ausencia de amigos es así:
Ahora eso puede parecer una diferencia muy discutible y todavía lo llamaría un "ayudante" (en un sentido posiblemente despectivo, ya que todavía estamos pasando todo el estado interno de la clase a la función, ya sea que lo necesite todo o no) excepto que evita el factor de "choque" del encuentro
friend
. En general,friend
parece un poco aterrador ver con frecuencia la ausencia de una inspección adicional, ya que dice que los elementos internos de su clase son accesibles en otro lugar (lo que implica que podría ser incapaz de mantener sus propios invariantes). Con la forma en que lo usa,friend
se vuelve bastante discutible si las personas conocen la práctica desdefriend
solo reside en el mismo archivo fuente que ayuda a implementar la funcionalidad privada de la clase, pero lo anterior logra el mismo efecto al menos con el beneficio posiblemente discutible de que no involucra a ningún amigo que evite todo ese tipo ("Oh dispara, esta clase tiene un amigo. ¿Dónde más se accede / muta a sus partes privadas? "). Mientras que la versión inmediatamente anterior comunica inmediatamente que no hay forma de acceder / mutar a los privados fuera de todo lo que se haga en la implementación dePredicateList
.Tal vez se esté moviendo hacia territorios algo dogmáticos con este nivel de matiz, ya que cualquiera puede descubrir rápidamente si nombra de manera uniforme las cosas
*Helper*
y las coloca en el mismo archivo fuente que todo se agrupa como parte de la implementación privada de una clase. Pero si nos volvemos quisquillosos, entonces quizás el estilo inmediatamente anterior no causará tanta reacción instintiva a simple vista sin lafriend
palabra clave que tiende a parecer un poco aterradora.Para las otras preguntas:
Esa podría ser una posibilidad a través de los límites de la API donde el cliente podría definir una segunda clase con el mismo nombre y obtener acceso a las partes internas de esa manera sin errores de enlace. Por otra parte, en gran medida soy un codificador en C que trabaja en gráficos donde las preocupaciones de seguridad en este nivel de "qué pasaría si" están muy bajas en la lista de prioridades, por lo que las preocupaciones como estas son solo las que tiendo a agitar mis manos y bailar y intenta fingir que no existen. :-D Si está trabajando en un dominio donde las preocupaciones como estas son bastante serias, creo que es una consideración decente.
La propuesta alternativa anterior también evita sufrir este problema. Sin embargo, si todavía desea seguir usando
friend
, también puede evitar ese problema haciendo que el asistente sea una clase privada anidada.Ninguno que yo sepa. Dudo que haya una, ya que realmente se está metiendo en los detalles de implementación y estilo.
"Ayudante del infierno"
Recibí una solicitud de mayor aclaración sobre el punto de cómo a veces me avergüenzo cuando veo implementaciones con mucho código "auxiliar", y eso podría ser un poco controvertido con algunos, pero en realidad es un hecho, ya que realmente me avergoncé cuando estaba depurando algunos de la implementación de mis colegas de una clase solo para encontrar un montón de "ayudantes" :-D Y no fui el único en el equipo rascándome la cabeza tratando de averiguar qué se supone que deben hacer exactamente todos estos ayudantes. Tampoco quiero salir dogmático como "No usarás ayudantes", pero haría una pequeña sugerencia de que podría ayudar pensar en cómo implementar cosas ausentes de ellos cuando sea práctico.
Y sí, estoy incluyendo métodos privados. Si veo una clase con una interfaz pública como sencillo, pero como un juego sin fin de métodos privados que son algo mal definido en el propósito, como
find_impl
ofind_detail
, ofind_helper
, entonces yo también temblar de una manera similar.Lo que estoy sugiriendo como alternativa son las funciones no miembro no miembro con enlace interno (declarado
static
o dentro de un espacio de nombres anónimo) para ayudar a implementar su clase con al menos un propósito más generalizado que "una función que ayuda a implementar otros". Y puedo citar Herb Sutter de C ++ 'Coding Standards' aquí por qué eso puede ser preferible desde un punto de vista general de SE:También puede comprender las "cuotas de membresía" de las que habla en cierto grado en términos del principio básico de reducir el alcance variable. Si imagina, como el ejemplo más extremo, un objeto de Dios que tiene todo el código requerido para que se ejecute todo su programa, entonces favorece a los "ayudantes" de este tipo (funciones, ya sean funciones de miembros o amigos) que pueden acceder a todos los elementos internos ( privados) de una clase básicamente hacen que esas variables no sean menos problemáticas que las variables globales. Tiene todas las dificultades para administrar el estado correctamente y la seguridad de subprocesos y mantener invariantes que obtendría con las variables globales en este ejemplo extremo. Y, por supuesto, la mayoría de los ejemplos reales no están tan cerca de este extremo, pero ocultar información es tan útil como limitar el alcance de la información a la que se accede.
Ahora Sutter ya da una buena explicación aquí, pero también agregaría que el desacoplamiento tiende a promover como una mejora psicológica (al menos si su cerebro funciona como el mío) en términos de cómo diseña las funciones. Cuando comienza a diseñar funciones que no pueden acceder a todo en la clase, excepto solo los parámetros relevantes que lo pasa o, si pasa la instancia de la clase como parámetro, solo sus miembros públicos, tiende a promover una mentalidad de diseño que favorece funciones que tienen un propósito más claro, además del desacoplamiento y la promoción de una encapsulación mejorada, de lo que de otro modo podría verse tentado a diseñar si pudiera acceder a todo.
Si volvemos a las extremidades, una base de código plagada de variables globales no tienta exactamente a los desarrolladores a diseñar funciones de una manera clara y generalizada en su propósito. Muy rápidamente, mientras más información pueda acceder en una función, muchos de nosotros los mortales nos enfrentamos a la tentación de desgeneralizarla y reducir su claridad a favor de acceder a toda esta información adicional que tenemos en lugar de aceptar parámetros más específicos y relevantes para esa función. para reducir su acceso al estado y ampliar su aplicabilidad y mejorar su claridad de intenciones. Eso se aplica (aunque generalmente en menor grado) con las funciones de miembros o amigos.
fuente
PredicateList
, a menudo podría ser factible simplemente pasar un miembro o dos de la lista de predicados a una función un poco más generalizada que no necesita acceso a cada miembro privado dePredicateList
, y a menudo eso tenderá a producir también un nombre y un propósito más claros y generalizados para esa función interna, así como más oportunidades para la "reutilización del código retrospectivo".