resolucion - ¿Definición de clase parcial en C++?
operador de resolucion de ambito c++ (18)
¿Alguien sabe si es posible tener una definición de clase parcial en C ++?
Algo como:
archivo1.h:
class Test { public: int test1(); };
archivo2.h:
class Test { public: int test2(); };
Para mí, parece bastante útil para definir clases multiplataforma que tienen funciones comunes entre ellas que son independientes de la plataforma porque la herencia es un costo a pagar que no es útil para clases multiplataforma.
Quiero decir que nunca tendrás dos instancias de especialización multiplataforma en tiempo de ejecución, solo en tiempo de compilación. La herencia puede ser útil para satisfacer sus necesidades de interfaz pública, pero después de eso no agregará nada útil en tiempo de ejecución, solo costos.
También tendrá que usar un #ifdef feo para usar la clase porque no puede hacer una instancia de una clase abstracta:
class genericTest { public: int genericMethod(); };
Entonces digamos para win32:
class win32Test: public genericTest { public: int win32Method(); };
Y tal vez:
class macTest: public genericTest { public: int macMethod(); };
Pensemos que tanto win32Method () como macMethod () llaman a genericMethod (), y tendrás que usar la clase como esta:
#ifdef _WIN32 genericTest *test = new win32Test(); #elif MAC genericTest *test = new macTest(); #endif test->genericMethod();
Ahora, pensando en un momento, la herencia solo fue útil para darles un método genericMethod () que depende de la plataforma específica, pero tiene el costo de llamar a dos constructores por eso. También tiene #ifdef feo dispersos por el código.
Es por eso que estaba buscando clases parciales. Pude en tiempo de compilación definir el extremo parcial dependiente de la plataforma específica, por supuesto que en este ejemplo tonto todavía necesito un #ifdef feo dentro de genericMethod () pero hay otras formas de evitar eso.
Las clases parciales son buenas si desea extender clases sin tocar los archivos originales. Por ejemplo, quiero extender la clase de plantilla de vector de std con ayudantes. ¿Por qué demonios tengo que crear una clase heredada como vectorEx y no solo agregar métodos a través de parcial?
Para mí, parece bastante útil para definir clases multiplataforma que tienen funciones comunes entre ellas que son independientes de la plataforma.
Excepto que los desarrolladores han estado haciendo esto durante décadas sin esta ''característica''.
Creo que parcial fue creado porque Microsoft ha tenido, durante décadas también, una mala costumbre de generar código y entregárselo a los desarrolladores para que lo desarrollen y mantengan.
El código generado es a menudo una pesadilla de mantenimiento. ¿Qué hábitos tiene ese marco completo generado por MFC cuando necesita superar su versión de MFC? ¿O cómo transfiere todo ese código en los archivos * .designer.cs cuando actualiza Visual Studio?
La mayoría de las otras plataformas dependen más de la generación de archivos de configuración que el usuario / desarrollador puede modificar. Esos, que tienen un vocabulario más limitado y no propensos a mezclarse con código no relacionado. Los archivos de configuración incluso se pueden insertar en el binario como un archivo de recursos si se considera necesario.
Nunca he visto el uso de "parcial" en un lugar donde la herencia o un archivo de recursos de configuración no hubieran hecho un mejor trabajo.
Como está escrito, no es posible.
Es posible que desee buscar en los espacios de nombres. Puede agregar una función a un espacio de nombres en otro archivo. El problema con una clase es que cada .cpp necesita ver el diseño completo de la clase.
Como los encabezados están insertados textualmente, uno de ellos podría omitir la "clase Prueba {" y "}" y estar #incluido en el medio del otro.
De hecho, he visto esto en el código de producción, aunque Delphi no C ++. En particular, me molestó porque rompió las funciones de navegación del código del IDE.
O use herencia, como dijo Jamie, o #ifdef para compilar diferentes partes en diferentes plataformas.
Puede obtener algo así como clases parciales usando la especialización de plantillas y la especialización parcial . Antes de invertir demasiado tiempo, verifique el soporte de su compilador. Los compiladores antiguos como MSC ++ 6.0 no admitían la especialización parcial.
Qué tal esto:
class WindowsFuncs { public: int f(); int winf(); };
class MacFuncs { public: int f(); int macf(); }
class Funcs
#ifdef Windows
: public WindowsFuncs
#else
: public MacFuncs
#endif
{
public:
Funcs();
int g();
};
Ahora Funcs
es una clase conocida en tiempo de compilación, por lo que no hay gastos generales causados por clases base abstractas o lo que sea.
o podrías probar PIMPL
archivo de encabezado común:
class Test
{
public:
...
void common();
...
private:
class TestImpl;
TestImpl* m_customImpl;
};
A continuación, cree los archivos cpp realizando las implementaciones personalizadas que son específicas de la plataforma.
#include will work as that is preprocessor stuff.
class Foo
{
#include "FooFile_Private.h"
}
////////
FooFile_Private.h:
private:
void DoSg();
No puede definir clases parcialmente en C ++.
Esta es una forma de obtener el efecto de "polimorfismo, donde solo hay una subclase", sin sobrecarga y con un mínimo de #define o duplicación de código. Se llama enlace dinámico simulado:
template <typename T>
class genericTest {
public:
void genericMethod() {
// do some generic things
std::cout << "Could be any platform, I dunno" << std::endl;
// base class can call a method in the child with static_cast
(static_cast<T*>(this))->doClassDependentThing();
}
};
#ifdef _WIN32
typedef Win32Test Test;
#elif MAC
typedef MacTest Test;
#endif
Luego, en otros encabezados, tendrás:
class Win32Test : public genericTest<Win32Test> {
public:
void win32Method() {
// windows-specific stuff:
std::cout << "I''m in windows" << std::endl;
// we can call a method in the base class
genericMethod();
// more windows-specific stuff...
}
void doClassDependentThing() {
std::cout << "Yep, definitely in windows" << std::endl;
}
};
y
class MacTest : public genericTest<MacTest> {
public:
void macMethod() {
// mac-specific stuff:
std::cout << "I''m in MacOS" << std::endl;
// we can call a method in the base class
genericMethod();
// more mac-specific stuff...
}
void doClassDependentThing() {
std::cout << "Yep, definitely in MacOS" << std::endl;
}
};
Esto le proporciona el polimorfismo apropiado en el momento de la compilación. genericTest no puede prácticamente invocar a doClassDependentThing de forma que le proporcione la versión de plataforma (casi como un método virtual), y cuando win32Method llama a genericMethod, por supuesto, obtiene la versión de clase base.
Esto no genera gastos indirectos asociados con las llamadas virtuales; obtienes el mismo rendimiento que si escribieras dos grandes clases sin código compartido. Puede crear una sobrecarga de llamada no virtual en con (de) destrucción, pero si el con (de) constructor para genericTest está en línea, debería estar bien, y esa sobrecarga en ningún caso es peor que tener un método genericInit que es invocado por ambas plataformas.
El código de cliente solo crea instancias de prueba y puede llamar a métodos que están en generic test o en la versión correcta para la plataforma. Para ayudar con el tipo de seguridad en el código que no se preocupa por la plataforma y no quiere hacer uso accidentalmente de llamadas específicas de la plataforma, también puede hacer:
#ifdef _WIN32
typedef genericTest<Win32Test> BaseTest;
#elif MAC
typedef genericTest<MacTest> BaseTest;
#endif
Tienes que ser un poco cuidadoso al utilizar BaseTest, pero no mucho más de lo que siempre es el caso con las clases base en C ++. Por ejemplo, no lo cortes con un valor de paso sin valor juzgado. Y no lo instaure directamente, porque si lo hace y llama a un método que termina intentando una llamada "falsa virtual", tiene problemas. Esto último se puede aplicar asegurando que todos los constructores de genericTest estén protegidos.
Esto no es posible en C ++, le dará un error sobre la redefinición de las clases ya definidas. Si desea compartir el comportamiento, considere la herencia.
Estoy de acuerdo en esto. Las clases parciales son construcciones extrañas que hacen que sea muy difícil mantenerlas después. Es difícil ubicar en qué clase parcial se declara cada miembro y es difícil evitar la redefinición o incluso la reimplementación de las características.
¿Desea extender el std :: vector, debe heredar de él? Esto se debe a varias razones. En primer lugar, cambia la responsabilidad de la clase y (¿correctamente?) Sus invariantes de clase. En segundo lugar, desde el punto de vista de la seguridad, esto debe evitarse. Considere una clase que maneje autenticación de usuario ...
partial class UserAuthentication {
private string user;
private string password;
public bool signon(string usr, string pwd);
}
partial class UserAuthentication {
private string getPassword() { return password; }
}
Se pueden mencionar muchas otras razones ...
Deje que las clases / funciones independientes de la plataforma y de la plataforma sean sus clases / funciones amigables. :)
Y sus identificadores de nombre separados permiten un control más fino sobre la instanciación, por lo que el acoplamiento es más flexible. La fundamentación de encapsulación de cortes parciales de OO es demasiado absoluta, mientras que las declaraciones de amigo necesarias apenas lo relajan lo suficiente como para facilitar la separación de paradigmas múltiples de aspectos específicos de plataforma de plataformas independientes de dominio específico.
De manera sucia pero práctica es el uso del preprocesador #include:
Test.h:
#ifndef TEST_H
#define TEST_H
class Test
{
public:
Test(void);
virtual ~Test(void);
#include "Test_Partial_Win32.h"
#include "Test_Partial_OSX.h"
};
#endif // !TEST_H
Test_Partial_OSX.h:
// This file should be included in Test.h only.
#ifdef MAC
public:
int macMethod();
#endif // MAC
Test_Partial_WIN32.h:
// This file should be included in Test.h only.
#ifdef _WIN32
public:
int win32Method();
#endif // _WIN32
Test.cpp:
// Implement common member function of class Test in this file.
#include "stdafx.h"
#include "Test.h"
Test::Test(void)
{
}
Test::~Test(void)
{
}
Test_Partial_OSX.cpp:
// Implement OSX platform specific function of class Test in this file.
#include "stdafx.h"
#include "Test.h"
#ifdef MAC
int Test::macMethod()
{
return 0;
}
#endif // MAC
Test_Partial_WIN32.cpp:
// Implement WIN32 platform specific function of class Test in this file.
#include "stdafx.h"
#include "Test.h"
#ifdef _WIN32
int Test::win32Method()
{
return 0;
}
#endif // _WIN32
He estado haciendo algo similar en mi motor de renderizado. Tengo una clase de interfaz IResource con plantilla de la que heredan una variedad de recursos (desglosados por brevedad):
template <typename TResource, typename TParams, typename TKey>
class IResource
{
public:
virtual TKey GetKey() const = 0;
protected:
static shared_ptr<TResource> Create(const TParams& params)
{
return ResourceManager::GetInstance().Load(params);
}
virtual Status Initialize(const TParams& params, const TKey key, shared_ptr<Viewer> pViewer) = 0;
};
La función Create
estática llama a una clase ResourceManager con plantilla que se encarga de cargar, descargar y almacenar instancias del tipo de recurso que administra con claves exclusivas, asegurando que las llamadas duplicadas se recuperen simplemente de la tienda, en lugar de volver a cargarlas como recursos separados.
template <typename TResource, typename TParams, typename TKey>
class TResourceManager
{
sptr<TResource> Load(const TParams& params) { ... }
};
Las clases de recursos concretas heredan de IResource utilizando el CRTP. Los ResourceManagers especializados para cada tipo de recurso se declaran como amigos de esas clases, de modo que la función Load
del ResourceManager pueda invocar la función Initialize
del recurso concreto. Uno de esos recursos es una clase de texturas, que además utiliza una expresión de pImpl para ocultar sus partes privadas:
class Texture2D : public IResource<Texture2D , Params::Texture2D , Key::Texture2D >
{
typedef TResourceManager<Texture2D , Params::Texture2D , Key::Texture2D > ResourceManager;
friend class ResourceManager;
public:
virtual Key::Texture2D GetKey() const override final;
void GetWidth() const;
private:
virtual Status Initialize(const Params::Texture2D & params, const Key::Texture2D key, shared_ptr<Texture2D > pTexture) override final;
struct Impl;
unique_ptr<Impl> m;
};
Gran parte de la implementación de nuestra clase de texturas es independiente de la plataforma (como la función GetWidth
si solo devuelve un int almacenado en el Impl). Sin embargo, dependiendo de qué API de gráficos tengamos como objetivo (por ejemplo, Direct3D11 vs. OpenGL 4.3), algunos de los detalles de implementación pueden diferir. Una solución podría ser heredar de IResource una clase intermedia Texture2D que define la interfaz pública extendida para todas las texturas, y luego heredar una clase D3DTexture2D y OGLTexture2D de eso. El primer problema con esta solución es que requiere que los usuarios de su API estén constantemente atentos sobre qué API de gráficos están orientando (podrían llamar a Create
en ambas clases secundarias). Esto podría resolverse restringiendo la clase Create
a intermediario Texture2D, que usa quizás un #ifdef
para crear un objeto secundario D3D u OGL. Pero aún existe el segundo problema con esta solución, que es que el código independiente de la plataforma se duplicaría en ambos niños, lo que ocasionaría esfuerzos adicionales de mantenimiento. Podría intentar resolver este problema moviendo el código independiente de la plataforma a la clase intermedia, pero ¿qué sucede si algunos de los datos de los miembros son utilizados tanto por el código específico de la plataforma como por el independiente de la plataforma? Los niños D3D / OGL no podrán acceder a los miembros de datos en Impl del intermediario, por lo que tendrían que moverlos de la Impl y al encabezado, junto con las dependencias que llevan, exponiendo a cualquier persona que incluya su encabezado a toda esa basura que no necesitan saber.
Las API deben ser fáciles de usar, correctas y difíciles de usar. Parte de ser fácil de usar es restringir la exposición del usuario a solo las partes de la API que debería usar. Esta solución lo abre para que se use fácilmente de forma incorrecta y agrega gastos generales de mantenimiento. Los usuarios solo deberían preocuparse por la API de gráficos a la que apuntan en un solo lugar, no en todos los sitios donde usan su API, y no deberían estar expuestos a sus dependencias internas. Esta situación demanda clases parciales, pero no están disponibles en C ++. Por lo tanto, puede definir la estructura Impl en archivos de encabezado separados, uno para D3D y otro para OGL, y colocar un #ifdef
en la parte superior del archivo Texture2D.cpp y definir el resto de la interfaz pública universalmente. De esta forma, la interfaz pública tiene acceso a los datos privados que necesita, el único código duplicado son las declaraciones de los miembros de datos (la construcción aún se puede hacer en el constructor Texture2D que crea el Impl), sus dependencias privadas se mantienen privadas y los usuarios no tiene que importar cualquier cosa excepto usar el conjunto limitado de llamadas en la superficie API expuesta:
// D3DTexture2DImpl.h
#include "Texture2D.h"
struct Texture2D::Impl
{
/* insert D3D-specific stuff here */
};
// OGLTexture2DImpl.h
#include "Texture2D.h"
struct Texture2D::Impl
{
/* insert OGL-specific stuff here */
};
// Texture2D.cpp
#include "Texture2D.h"
#ifdef USING_D3D
#include "D3DTexture2DImpl.h"
#else
#include "OGLTexture2DImpl.h"
#endif
Key::Texture2D Texture2D::GetKey() const
{
return m->key;
}
// etc...
Declarar un cuerpo de clase dos veces probablemente genere un error de redefinición de tipo. Si estás buscando una solución alternativa. Sugiero # ifdef''ing, o el uso de una clase base abstracta para ocultar detalles específicos de la plataforma.
Esto no es posible en C ++, le dará un error sobre la redefinición de las clases ya definidas. Si desea compartir el comportamiento, considere la herencia.
Nop.
Pero, es posible que desee buscar una técnica llamada "Clases de políticas". Básicamente, usted hace microclases (que no son útiles por sí mismos) y luego los pega en un momento posterior.
Prueba herencia
Específicamente
class AllPlatforms {
public:
int common();
};
y entonces
class PlatformA : public AllPlatforms {
public:
int specific();
};