2009-04-22 10 views
19

¿Es posible crear un archivo de encabezado C++ (.h) que declare una clase y sus métodos públicos, pero no define los miembros privados en esa clase? Encontré un few pages que dice que debes declarar la clase y todos sus miembros en el archivo de encabezado, luego define los métodos por separado en tu archivo cpp. Lo pido porque quiero tener una clase que esté definida en una DLL Win32, y quiero que esté encapsulada correctamente: la implementación interna de esa clase podría cambiar, incluidos sus miembros, pero estos cambios no deberían afectar el código que usa la clase .C++ Archivo de encabezado que declara una clase y métodos pero no miembros?

Supongo que si tuviera esto, sería imposible para el compilador conocer el tamaño de mis objetos antes de tiempo. Pero eso debería estar bien, siempre que el compilador sea lo suficientemente inteligente como para usar el constructor y simplemente pase los punteros a la ubicación en la memoria donde está almacenado mi objeto, y nunca me permita ejecutar "sizeof (MyClass)".

Actualización: Gracias a todos los que respondieron! Parece que el idioma pimpl es una buena forma de lograr lo que estaba hablando. Voy a hacer algo similar:

archivo

Mi Win32 DLL tendrá un montón de funciones separadas de esta manera:

void * __stdcall DogCreate(); 
int __stdcall DogGetWeight(void * this); 
void __stdcall DogSetWeight(void * this, int weight); 

Esta es la forma típica de Microsoft escribe sus archivos DLL así que creo que es probablemente una buena razón para eso.

Pero quiero aprovechar la agradable sintaxis que C++ tiene para las clases, así que escribiré una clase contenedora para concluir todas estas funciones. Tendrá un miembro, que será "vacid * pimpl". Esta clase de contenedor será tan simple que también podría simplemente declararlo Y definirlo en el archivo de encabezado. Pero esta clase de contenedor realmente no tiene otro objetivo que no sea hacer que el código de C++ se vea bonito por lo que yo sé.

Respuesta

33

Creo que lo que estamos buscando es algo que se llama el "lenguaje pimpl". Para entender cómo funciona esto, debes entender que en C++ puedes reenviar declarar algo así.

class CWidget; // Widget will exist sometime in the future 
CWidget* aWidget; // An address (integer) to something that 
        // isn't defined *yet* 

// later on define CWidget to be something concrete 
class CWidget 
{ 
    // methods and such 
}; 

Así que para reenviar declarar medios para prometer a declarar un tipo totalmente después. Se dice que "habrá algo llamado CWidget, lo prometo. Te contaré más sobre esto más adelante".

Las reglas de declaración adelantada dicen que se puede definir un puntero o una referencia a algo que se ha declarado hacia delante. Esto se debe a que los punteros y las referencias son en realidad direcciones simples, un número en el que lo que está por definirse será. Poder declarar un puntero a algo sin declararlo por completo es conveniente por muchas razones.

Su útil en este caso porque se puede usar esto para ocultar algunas de las partes internas de una clase utilizando el método de "pimpl". Pimpl significa "indicador de implementación". Entonces, en lugar de "widget" tienes una clase que es la implementación real. La clase que está declarando en su encabezado es solo un paso a través de la clase CImpl. He aquí cómo funciona:

// Thing.h 

class CThing 
{ 
public: 
    // CThings methods and constructors... 
    CThing(); 
    void DoSomething(); 
    int GetSomething(); 
    ~CThing(); 
private: 
    // CThing store's a pointer to some implementation class to 
    // be defined later 
    class CImpl;  // forward declaration to CImpl 
    CImpl* m_pimpl; // pointer to my implementation 
}; 

Thing.cpp tiene métodos de CThing definidos como pasar-through a la impl:

// Fully define Impl 
class CThing::CImpl 
{ 
private: 
    // all variables 
public: 
    // methods inlined 
    CImpl() 
    { 
      // constructor 
    } 

    void DoSomething() 
    { 
      // actual code that does something 
    } 
    //etc for all methods  
}; 

// CThing methods are just pass-throughs 
CThing::CThing() : m_pimpl(new CThing::CImpl()); 
{ 
} 

CThing::~CThing() 
{ 
    delete m_pimpl; 
} 

int CThing::GetSomething() 
{ 
    return m_pimpl->GetSomething(); 
} 

void CThing::DoSomething() 
{ 
    m_impl->DoSomething(); 
} 

Tada! Has ocultado todos los detalles en tu cpp y tu archivo de encabezado es una lista muy ordenada de métodos. Es una gran cosa. Lo único que puede ver diferente de la plantilla anterior es que las personas pueden usar boost :: shared_ptr <> u otro puntero inteligente para la impl. Algo que se elimina a sí mismo.

Además, tenga en cuenta que este método presenta algunas molestias. La depuración puede ser un poco molesto (nivel extra de redirección para avanzar). También es una gran cantidad de gastos generales para crear una clase. Si haces esto para cada clase, te cansarás de todo el tipeo :).

3

Google "modismo de espinillas" o "manejar C++".

+0

Mejor deletreado 'pimpl' (para 'implementación privada')? –

3

Sí, esto puede ser una actividad deseable. Una manera fácil es hacer que la clase de implementación se derive de la clase definida en el encabezado.

El inconveniente es que el compilador no sabrá cómo construir su clase, por lo que necesitará algún tipo de método de fábrica para obtener instancias de la clase. Será imposible tener instancias locales en la pila.

-1

Mira la clase The Handle-Body Idiom en C++

+1

Su enlace ahora está roto. –

0

¿Es posible hacer una archivo C++ cabecera (.h) que declara una clase, y sus métodos públicos, pero no lo hace declaran lo privado miembros en esa clase ?

La respuesta más cercana es la del idioma PIMPL.

Consulte esto The Fast Pimpl Idiom de Herb Sutter.

IMO Pimpl es realmente útil durante las etapas iniciales de desarrollo donde su archivo de encabezado va a cambiar muchas veces. Pimpl tiene su costo debido a su asignación \ desasignación de objeto interno en montón.

7

pimpl idiom agrega un miembro de datos privados void * a su clase, y esta es una técnica útil si necesita algo rápido & sucio. Sin embargo, tiene sus inconvenientes. El principal de ellos es que dificulta el uso del polimorfismo en el tipo abstracto. A veces puede querer una clase base abstracta y subclases de esa clase base, recopilar punteros a todos los diferentes tipos en un vector y llamar a métodos sobre ellos. Además, si el propósito del idioma pimpl es ocultar los detalles de implementación de la clase, entonces solo tiene casi: el puntero en sí es un detalle de implementación. Un detalle de implementación opaco, tal vez. Pero un detalle de implementación, no obstante.

Una alternativa a la expresión idiomática pimpl existe que puede ser utilizado para eliminar todos los detalles de la implementación de la interfaz mientras que proporciona un tipo de base que se puede utilizar polimórficamente, si es necesario.

En el archivo de cabecera del archivo DLL (el #include por código de cliente) crear una clase abstracta con sólo métodos y conceptos públicas que dictan cómo la clase es crear una instancia (por ejemplo, métodos de fábrica públicas & métodos clon):

kennel.h

/**************************************************************** 
*** 
*** The declaration of the kennel namespace & its members 
*** would typically be in a header file. 
***/ 

// Provide an abstract interface class which clients will have pointers to. 
// Do not permit client code to instantiate this class directly. 

namespace kennel 
{ 
    class Animal 
    { 
    public: 
     // factory method 
     static Animal* createDog(); // factory method 
     static Animal* createCat(); // factory method 

     virtual Animal* clone() const = 0; // creates a duplicate object 
     virtual string speak() const = 0; // says something this animal might say 
     virtual unsigned long serialNumber() const = 0; // returns a bit of state data 
     virtual string name() const = 0; // retuyrns this animal's name 
     virtual string type() const = 0; // returns the type of animal this is 

     virtual ~Animal() {}; // ensures the correct subclass' dtor is called when deleteing an Animal* 
    }; 
}; 

... animales es un abstract base class y por lo tanto no puede ser instanciada; no es necesario declarar un ctor privado.La presencia del dtor virtual asegura que si alguien delete es un Animal*, también se llamará a la subclase adecuada 'dtor.

Para implementar diferentes subclases del tipo base (por ejemplo, perros & gatos), declararía las clases de nivel de implementación en su DLL. Estas clases derivan en última instancia de la clase base abstracta que usted declaró en su archivo de encabezado, y los métodos de fábrica en realidad crearían una instancia de una de estas subclases.

dll.cpp:

/**************************************************************** 
*** 
*** The code that follows implements the interface 
*** declared above, and would typically be in a cc 
*** file. 
***/ 

// Implementation of the Animal abstract interface 
// this implementation includes several features 
// found in real code: 
//  Each animal type has it's own properties/behavior (speak) 
//  Each instance has it's own member data (name) 
//  All Animals share some common properties/data (serial number) 
// 

namespace 
{ 
    // AnimalImpl provides properties & data that are shared by 
    // all Animals (serial number, clone) 
    class AnimalImpl : public kennel::Animal  
    { 
    public: 
     unsigned long serialNumber() const; 
     string type() const; 

    protected: 
     AnimalImpl(); 
     AnimalImpl(const AnimalImpl& rhs); 
     virtual ~AnimalImpl(); 
    private: 
     unsigned long serial_;    // each Animal has its own serial number 
     static unsigned long lastSerial_; // this increments every time an AnimalImpl is created 
    }; 

    class Dog : public AnimalImpl 
    { 
    public: 
     kennel::Animal* clone() const { Dog* copy = new Dog(*this); return copy;} 
     std::string speak() const { return "Woof!"; } 
     std::string name() const { return name_; } 

     Dog(const char* name) : name_(name) {}; 
     virtual ~Dog() { cout << type() << " #" << serialNumber() << " is napping..." << endl; } 
    protected: 
     Dog(const Dog& rhs) : AnimalImpl(rhs), name_(rhs.name_) {}; 

    private: 
     std::string name_; 
    }; 

    class Cat : public AnimalImpl 
    { 
    public: 
     kennel::Animal* clone() const { Cat* copy = new Cat(*this); return copy;} 
     std::string speak() const { return "Meow!"; } 
     std::string name() const { return name_; } 

     Cat(const char* name) : name_(name) {}; 
     virtual ~Cat() { cout << type() << " #" << serialNumber() << " escaped!" << endl; } 
    protected: 
     Cat(const Cat& rhs) : AnimalImpl(rhs), name_(rhs.name_) {}; 

    private: 
     std::string name_; 
    }; 
}; 

unsigned long AnimalImpl::lastSerial_ = 0; 


// Implementation of interface-level functions 
// In this case, just the factory functions. 
kennel::Animal* kennel::Animal::createDog() 
{ 
    static const char* name [] = {"Kita", "Duffy", "Fido", "Bowser", "Spot", "Snoopy", "Smkoky"}; 
    static const size_t numNames = sizeof(name)/sizeof(name[0]); 

    size_t ix = rand()/(RAND_MAX/numNames); 

    Dog* ret = new Dog(name[ix]); 
    return ret; 
} 

kennel::Animal* kennel::Animal::createCat() 
{ 
    static const char* name [] = {"Murpyhy", "Jasmine", "Spike", "Heathcliff", "Jerry", "Garfield"}; 
    static const size_t numNames = sizeof(name)/sizeof(name[0]); 

    size_t ix = rand()/(RAND_MAX/numNames); 

    Cat* ret = new Cat(name[ix]); 
    return ret; 
} 


// Implementation of base implementation class 
AnimalImpl::AnimalImpl() 
: serial_(++lastSerial_) 
{ 
}; 

AnimalImpl::AnimalImpl(const AnimalImpl& rhs) 
: serial_(rhs.serial_) 
{ 
}; 

AnimalImpl::~AnimalImpl() 
{ 
}; 

unsigned long AnimalImpl::serialNumber() const 
{ 
    return serial_; 
} 

string AnimalImpl::type() const 
{ 
    if(dynamic_cast<const Dog*>(this)) 
     return "Dog"; 
    if(dynamic_cast<const Cat*>(this)) 
     return "Cat"; 
    else 
     return "Alien"; 
} 

Ahora usted tiene la interfaz definida en la cabecera & los detalles de implementación completamente separados de dónde código de cliente no puede ver nada en absoluto. Debería usar esto llamando a los métodos declarados en su archivo de encabezado desde el código que enlaza con su DLL. He aquí una muestra de controlador:

main.cpp:

std::string dump(const kennel::Animal* animal) 
{ 
    stringstream ss; 
    ss << animal->type() << " #" << animal->serialNumber() << " says '" << animal->speak() << "'" << endl; 
    return ss.str(); 
} 

template<class T> void del_ptr(T* p) 
{ 
    delete p; 
} 

int main() 
{ 
    srand((unsigned) time(0)); 

    // start up a new farm 
    typedef vector<kennel::Animal*> Animals; 
    Animals farm; 

    // add 20 animals to the farm 
    for(size_t n = 0; n < 20; ++n) 
    { 
     bool makeDog = rand()/(RAND_MAX/2) != 0; 
     if(makeDog) 
      farm.push_back(kennel::Animal::createDog()); 
     else 
      farm.push_back(kennel::Animal::createCat()); 
    } 

    // list all the animals in the farm to the console 
    transform(farm.begin(), farm.end(), ostream_iterator<string>(cout, ""), dump); 

    // deallocate all the animals in the farm 
    for_each(farm.begin(), farm.end(), del_ptr<kennel::Animal>); 

    return 0; 
} 
+2

Una liendre: no hay necesidad de que el puntero de pimpl sea un vacío *. Todo lo que se necesita hacer es que el encabezado público reenvíe la referencia a la clase pimpl. –

2

Usted tiene que declarar todos los miembros de la cabecera por lo que el compilador sabe qué tan grande es el objeto y así sucesivamente.

Pero se puede resolver mediante el uso de una interfaz:

ext.h:

class ExtClass 
{ 
public: 
    virtual void func1(int xy) = 0; 
    virtual int func2(XYClass &param) = 0; 
}; 

int.h:

class ExtClassImpl : public ExtClass 
{ 
public: 
    void func1(int xy); 
    int func2(XYClass&param); 
}; 

int.cpp:

void ExtClassImpl::func1(int xy) 
    { 
    ... 
    } 
    int ExtClassImpl::func2(XYClass&param) 
    { 
    ... 
    } 
Cuestiones relacionadas