usuario tipos por plantillas funciones estándar estandar definidas biblioteca c++ templates callback readability maintainability

tipos - funciones estandar en c++



¿Cómo auto-documentar una función de devolución de llamada que es llamada por la clase de biblioteca de plantillas? (9)

Aquí hay una solución usando una clase de Rasgos:

// Library.h: template<class T> struct LibraryTraits; // must be implemented for every User-class template<class T> class Library { public: T* node=nullptr; void utility() { LibraryTraits<T>::func(node); } }; // User.h: class User { }; // must only be implemented if User is to be used by Library (and can be implemented somewhere else) template<> struct LibraryTraits<User> { static void func(User* node) { std::cout << "LibraryTraits<User>::func(" << node << ")/n"; } }; // main.cpp: int main() { Library<User> li; li.utility(); }

Ventajas:

  • Es obvio al nombrar que LibraryTraits<User> solo es necesario para la interfaz de User por Library (y se puede eliminar, una vez que se elimine Library o User .
  • LibraryTraits puede ser especializado independiente de Library y User

Desventajas:

  • No es fácil acceder a los miembros privados del User (hacer de LibraryTraits un amigo del User eliminaría la independencia).
  • Si se necesita la misma func para las diferentes clases de la Library , se deben implementar múltiples clases de Trait (se podrían resolver con implementaciones predeterminadas heredadas de otras clases de Trait ).

Tengo una función User::func() (devolución de llamada) a la que llamaría una clase de plantilla ( Library<T> ).

En la primera iteración del desarrollo, todos sabemos que func() sirve solo para ese único propósito.
Unos meses después, la mayoría de los miembros olvidan para qué sirve func() .
Después de una fuerte refactorización, la func() veces es eliminada por algunos codificadores.

Al principio, no pensé que esto fuera un problema en absoluto.
Sin embargo, después de reencontrarme con este patrón varias veces, creo que necesito algunas contramedidas.

Pregunta

¿Cómo documentarlo elegantemente? (lindo && conciso && sin costo adicional de CPU)

Ejemplo

Aquí hay un código simplificado:
(El problema del mundo real es la dispersión de más de 10 archivos de biblioteca y más de 20 archivos de usuario y más de 40 funciones).

Biblioteca.h

template<class T> class Library{ public: T* node=nullptr; public: void utility(){ node->func(); //#1 } };

Usuario.h

class User{ public: void func(){/** some code*/} //#1 //... a lot of other functions ... // some of them are also callback of other libraries };

main.cpp

int main(){ Library<User> li; .... ; li.utility(); }

Mis pobres soluciones

1. Comentario / doc.

Como la primera solución, tiendo a agregar un comentario como este:

class User{ /** This function is for "Library" callback */ public: void func(){/** some code*/} };

Pero se ensucia bastante rápido, tengo que agregarlo a cada "función" en cada clase.

2. Renombra la "func ()"

En el caso real, tiendo a prefijar un nombre de función como este:

class User{ public: void LIBRARY_func(){/** some code*/} };

Es muy notable, pero el nombre de la función es ahora mucho más largo.
(especialmente cuando Library -class tiene un nombre de clase más largo)

3. Clase virtual con "func () = 0"

Estoy considerando crear una clase abstracta como interfaz para la devolución de llamada.

class LibraryCallback{ public: virtual void func()=0; }; class User : public LibraryCallback{ public: virtual void func(){/** some code*/} };

Proporciona la sensación de que func() es para algo bastante externo . :)
Sin embargo, tengo que sacrificar el costo de las llamadas virtuales (v-table).
En casos críticos de rendimiento, no puedo pagarlo.

4. Función estática

(idea de Daniel Jour en comentario, gracias!)

Casi 1 mes después, aquí está cómo uso: -

Biblioteca.h

template<class T> class Library{ public: T* node=nullptr; public: void utility(){ T::func(node); //#1 } };

Usuario.h

class User{ public: static void func(Callback*){/** some code*/} };

main.cpp

int main(){ Library<User> li; }

Probablemente sea más limpio, pero aún carece de auto-documento.


Desde el lado del usuario, usaría crtp para crear una interfaz de devolución de llamada y obligaría a los usuarios a usarla. Por ejemplo:

template <typename T> struct ICallbacks { void foo() { static_cast<T*>(this)->foo(); } };

Los usuarios deben heredar de esta interfaz e implementar la devolución de llamada foo()

struct User : public ICallbacks<User> { void foo() {std::cout << "User call back" << std::endl;} };

Lo bueno de esto es que si Library utiliza la interfaz ICallback y el User olvida de implementar foo() , recibirá un mensaje de error del compilador.

Tenga en cuenta que no hay una función virtual, por lo que no hay penalización de rendimiento aquí

Desde el lado de la biblioteca, solo llamaría a esas devoluciones de llamada a través de sus interfaces (en este caso, ICallback ). Siguiendo OP en el uso de punteros, haría algo como esto:

template <typename T> struct Library { ICallbacks<T> *node = 0; void utility() { assert(node != nullptr); node->foo(); } };

Tenga en cuenta que las cosas se documentan automáticamente de esta manera. Es muy explícito que esté utilizando una interfaz de devolución de llamada, y el node es el objeto que tiene esas funciones.

A continuación un ejemplo completo de trabajo:

#include <iostream> #include <cassert> template <typename T> struct ICallbacks { void foo() { static_cast<T*>(this)->foo(); } }; struct User : public ICallbacks<User> { void foo() {std::cout << "User call back" << std::endl;} }; template <typename T> struct Library { ICallbacks<T> *node = 0; void utility() { assert(node != nullptr); node->foo(); } }; int main() { User user; Library<User> l; l.node = &user; l.utility(); }


Esto recuerda en gran medida a un diseño antiguo basado en políticas , excepto que en su caso no hereda la clase de Library clase de User . Los buenos nombres son los mejores amigos de cualquier API. Combine esto con el conocido patrón de diseño basado en políticas (bien conocido es muy importante porque los nombres de clase con la palabra Policy en él sonarán inmediatamente en muchos lectores del código) y, supongo, obtendrá una código bien autodocumentado.

  • La herencia no le dará ninguna sobrecarga de rendimiento, pero le dará la posibilidad de tener la Callback como un método protegido, lo que le dará algún indicio de que se debe heredar y usar en algún lugar.

  • Tenga claramente una SomePolicyOfSomething y una nomenclatura coherente entre varias clases de tipo User (por ejemplo, SomePolicyOfSomething en la forma del Diseño Basado en Políticas mencionado anteriormente), así como, los argumentos de plantilla para la Library (por ejemplo, SomePolicy , o lo llamaría TSomePolicy ).

  • El using declaración de Callback de Callback en la clase de la Library puede dar errores mucho más claros y más tempranos (por ejemplo, desde IDE o clang moderno, analizadores de sintaxis de visial studio para IDE).

Otra opción discutible podría ser static_assert si tiene C ++> = 11. Pero en este caso, se debe utilizar en todas las clases de tipo User ((.


La clase abstracta es la mejor manera de imponer la función para no ser eliminada. Por lo tanto, recomiendo implementar la clase base con la función virtual pura, por lo que derivada tiene que definir la función. O la segunda solución sería tener punteros de función para que el rendimiento se guarde al evitar la sobrecarga adicional de la creación de tablas V y las llamadas.


No es una respuesta directa a su pregunta sobre cómo documentarla, sino algo a considerar:

Si su plantilla de biblioteca requiere una implementación de someFunction() para cada clase que se usará en ella, recomendaría agregarla como un argumento de plantilla.

#include <functional> template<class Type, std::function<void(Type*)> callback> class Library { // Some Stuff... Type* node = nullptr; public: void utility() { callback(this->node); } };

Podría hacerlo aún más explícito, para que otros desarrolladores sepan que es necesario.


Si bien sé que no respondo a su pregunta específica (cómo documentar la función de no borrado), resolvería su problema (manteniendo la función de devolución de llamada aparentemente no utilizada en la base del código) creando una instancia de Library<User> y llamar a la función utility() en una prueba unitaria (o tal vez debería llamarse una prueba API ...). Es probable que esta solución también se adapte a su ejemplo del mundo real, siempre que no tenga que comprobar cada combinación posible de clases de biblioteca y funciones de devolución de llamada.

Si tiene la suerte de trabajar en una organización donde se requieren pruebas de unidad y revisión de código exitosas antes de que se introduzcan cambios en la base del código, esto requeriría un cambio en las pruebas de unidad antes de que alguien pueda eliminar la User::func() y tal El cambio probablemente atraería la atención de un revisor.

Nuevamente, usted conoce su entorno y yo no, y soy consciente de que esta solución no se ajusta a todas las situaciones.


Si no es obvio que func() es necesario en el User , entonces diría que está violando el principio de responsabilidad única . En su lugar, cree una clase de adapter de la cual User como miembro.

class UserCallback { public: void func(); private: User m_user; }

De esa manera, la existencia de documentos UserCallback que func() es una devolución de llamada externa, y separa la necesidad de devolución de llamada de la Library de las responsabilidades reales del User .


func no es una característica del User . Es una característica del acoplamiento User - Library<T> .

Colocarlo en User si no tiene una semántica clara fuera del uso de la Library<T> es una mala idea. Si tiene una semántica clara, debería decir lo que hace, y eliminarla debería ser una mala idea.

Colocarlo en la Library<T> no puede funcionar, porque su comportamiento es una función de la T en la Library<T> .

La respuesta es colocarlo en ningún punto.

template<class T> struct tag_t{ using type=T; constexpr tag_t(){} }; template<class T> constexpr tag_t<T> tag{};

Ahora en Library.h :

struct ForLibrary; template<class T> class Library{ public: T* node=nullptr; public: void utility(){ func( tag<ForLibrary>, node ); // #1 } };

en User.h :

struct ForLibrary; class User{ /** This function is for "Library" callback */ public: friend void func( tag_t<ForLibrary>, User* self ) { // code } };

o simplemente coloque esto en el mismo espacio de nombres como User , o el mismo espacio de nombres que ForLibrary :

friend func( tag_t<ForLibrary>, User* self );

Antes de borrar la func , localizará ForLibrary .

Ya no forma parte de la "interfaz pública" del User , por lo que no lo desordena. Es un amigo (un ayudante) o una función gratuita en el mismo espacio de nombre de User o Library .

Puede implementarlo donde necesite un Library<User> lugar de User.h o User.h , especialmente si solo utiliza interfaces públicas de User .

Las técnicas utilizadas aquí son "envío de etiquetas", "búsqueda dependiente del argumento", "funciones de amistad" y la preferencia de funciones libres sobre métodos.


Prueba.h

#ifndef TEST_H #define TEST_H // User Class Prototype Declarations class User; // Templated Wrapper Class To Contain Callback Functions // User Will Inherit From This Using Their Own Class As This // Class''s Template Parameter template <class T> class Wrapper { public: // Function Template For Callback Methods. template<class U> auto Callback(...) {}; }; // Templated Library Class Defaulted To User With The Utility Function // That Provides The Invoking Of The Call Back Method template<class T = User> class Library { public: T* node = nullptr; void utility() { T::Callback(node); } }; // User Class Inherited From Wrapper Class Using Itself As Wrapper''s Template Parameter. // Call Back Method In User Is A Static Method And Takes A class Wrapper* Declaration As // Its Parameter class User : public Wrapper<User> { public: static void Callback( class Wrapper* ) { std::cout << "Callback was called./n"; } }; #endif // TEST_H

main.cpp

#include "Test.h" int main() { Library<User> l; l.utility(); return 0; }

Salida

Callback was called.

Pude compilar, compilar y ejecutar esto sin error en VS2017 CE en Windows 7 - 64bit Intel Core 2 Quad Extreme.

¿Alguna idea?

Recomendaría asignar un nombre a la clase de envoltorio de manera adecuada, luego para cada función específica de devolución de llamada que tenga un propósito único, asigne un nombre correspondiente dentro de la clase de envoltorio.

Editar

Después de jugar con esta "plantilla mágica", bueno, no hay tal cosa ... Había comentado la plantilla de función en la clase Wrapper y descubrí que no es necesaria. Luego comenté la class Wrapper* que es la lista de argumentos para la Callback() en User . Esto me dio un error de compilación que indicaba que User::Callback() no toma 0 argumentos. Así que volví a mirar a Wrapper ya que el User hereda. Bueno, en este punto, Wrapper es una plantilla de clase vacía.

Esto me lleva a mirar la Library . La biblioteca tiene un puntero al User como miembro público y una función de utility() que invoca User''s método de static Callback User''s . Es aquí donde el método de invocación está llevando un puntero a un objeto User como su parámetro. Así que me llevo a probar esto:

class User; // Prototype class A{}; // Empty Class template<class T = User> class Library { public: T* node = nullptr; void utility() { T::Callback(node); } }; class User : public A { public: static void Callback( A* ) { std::cout << "Callback was called./n"; } };

Y esto compila y construye correctamente como la versión simplificada. Sin embargo; cuando lo pensé; La versión de la plantilla es mejor porque se deduce en tiempo de compilación y no en tiempo de ejecución. Entonces, cuando volvemos a usar las plantillas, javaLover me preguntó qué significa class Wrapper* o está dentro de la lista de argumentos para el método de Callback dentro de la clase de User .

Trataré de explicar esto tan claramente como pueda, pero primero la clase contenedora es solo una plantilla vacía que el User heredará y no hace nada más que actuar como una clase base y ahora se ve así:

template<class T> class Wrapper { // Could Be Changed To A More Suitable Name Such As Shell or BaseShell };

Cuando miramos la clase de User :

class User : public Wrapper<User> { public: static void Callback( class Wrapper* ) { // print statement } };

Vemos que el User es una clase sin plantilla que se hereda de una clase de plantilla pero se usa a sí misma como el argumento de la plantilla. Contiene un método estático público y este método no devuelve nada, pero toma un solo parámetro; esto también es evidente en la clase de Library que tiene su parámetro de plantilla como una clase de User . Cuando el método utility() Library''s invoca el método Callback() User''s el parámetro que la Library espera es un puntero a un objeto User . Entonces, cuando volvamos a la clase User lugar de declararlo como un puntero User* directamente en su declaración, estoy usando la plantilla de clase vacía de la que hereda. Sin embargo si intentas hacer esto:

class User : public Wrapper<User> { public: static void Callback( Wrapper* ) { // print statement } };

Debería recibir un mensaje de que a Wrapper* le falta la lista de argumentos. Podríamos simplemente hacer Wrapper<User>* aquí, pero eso es redundante ya que ya vemos que el Usuario está heredando de Wrapper que se lleva a cabo. Por lo tanto, podemos solucionarlo y hacerlo más limpio simplemente prefijando Wrapper* con la palabra clave de class ya que es una plantilla de clase. Por lo tanto, la plantilla de magia ... bueno, no hay magia aquí ... solo compilador intrínseco y optimizaciones.