Plik nagłówkowy C++, który deklaruje klasę i metody, ale nie członków?

Czy jest możliwe utworzenie pliku nagłówkowego C++ (.h) deklaruje klasę i jej publiczne metody, ale nie definiuje prywatnych członków tej klasy? Znalazłem kilka stron , które mówią, że należy zadeklarować klasę i wszystkich jej członków w pliku nagłówkowym, a następnie zdefiniować metody oddzielnie w pliku cpp. Pytam, ponieważ chcę mieć klasę, która jest zdefiniowana w DLL Win32 i chcę, aby była poprawnie zamknięta: wewnętrzna implementacja tej klasy może się zmienić, w tym jego członków, ale te zmiany nie powinny wpływać na kod, który używa klasy.

Myślę, że gdybym miał to, to nie byłoby możliwe, aby kompilator znał rozmiar moich obiektów z wyprzedzeniem. Ale to powinno być w porządku, o ile kompilator jest wystarczająco inteligentny, aby użyć konstruktora i po prostu przekazać wskaźniki do miejsca w pamięci, gdzie mój obiekt jest przechowywany, i nigdy nie pozwól mi uruchomić " sizeof (MyClass)".

Aktualizacja: dziękujemy wszystkim, którzy odpowiedzieli! Wydaje się na przykład idiom pimpl jest dobrym sposobem na osiągnięcie tego, o czym mówiłem. Mam zamiar zrobić coś podobnego:

Mój plik DLL Win32 będzie miał kilka oddzielnych funkcji, takich jak:

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

Jest to typowy sposób, w jaki Microsoft zapisuje swoje pliki DLL, więc myślę, że prawdopodobnie jest ku temu dobry powód.

Ale chcę skorzystać z ładnej składni C++ dla klas, więc napiszę klasę wrappera, aby zawinąć wszystkie te funkcje. Będzie miał jednego członka, który będzie be "void * pimpl". Ta klasa wrappera będzie tak prosta, że równie dobrze mogę ją zadeklarować i zdefiniować w pliku nagłówkowym. Ale ta klasa wrappera naprawdę nie ma innych celów niż uczynienie kodu C++ ładnym, o ile mogę powiedzieć.

Author: David Grayson, 2009-04-22

8 answers

Myślę, że to, czego szukasz, to coś, co nazywa się idiomem pimpl. Aby zrozumieć, jak to działa, musisz zrozumieć, że w C++ możesz zadeklarować coś takiego.

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 
};

Tak więc forward declare oznacza obietnicę pełnego zadeklarowania typu później. Jego powiedzenie " będzie coś, co nazywa się CWidget, obiecuję. Opowiem ci o tym później.".

Zasady deklaracji forward mówią, że można zdefiniować wskaźnik lub odniesienie do czegoś, co zostało / align = "left" / Dzieje się tak, ponieważ wskaźniki i odniesienia są tak naprawdę tylko adresami-liczbą, w której będzie ta jeszcze nie zdefiniowana rzecz. Możliwość zadeklarowania wskaźnika do czegoś bez pełnej deklaracji jest wygodna z wielu powodów.

Jest to przydatne tutaj, ponieważ możesz użyć tego, aby ukryć niektóre wewnętrzne elementy klasy za pomocą metody "pimpl". Pimpl oznacza "wskaźnik do implementacji". Więc zamiast "widget" masz klasę, która jest rzeczywistą implementacją. The class you are deklarowanie w nagłówku jest tylko przejściem do klasy CImpl. Oto jak to działa:

// 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
};

Rzecz.cpp ma metody Cthinga zdefiniowane jako przejścia do 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! Ukryłeś wszystkie szczegóły w cpp, a plik nagłówka to bardzo uporządkowana lista metod. To świetna rzecz. Jedyną rzeczą, którą możesz zobaczyć różni się od powyższego szablonu jest to, że ludzie mogą używać boost:: shared_ptr lub innego inteligentnego wskaźnika dla impl. Coś, co usuwa siebie.

Należy również pamiętać, że ta metoda wiąże się z pewnymi irytacjami. Debugowanie może być nieco irytujące (dodatkowy poziom przekierowania do step through). Jego również wiele kosztów na stworzenie klasy. Jeśli robisz to dla każdej klasy, znudzi ci się całe pisanie:).

 33
Author: Doug T.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-23 01:01:46

Użycie pimpl idiom.

 13
Author: bayda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-22 19:35:37

Idiom pimpl dodaje void* private data do twojej klasy i jest to przydatna technika, jeśli potrzebujesz czegoś szybkiego i brudnego. Ma jednak swoje wady. Głównym z nich jest to, że utrudnia zastosowanie polimorfizmu na typie abstrakcyjnym. Czasami możesz potrzebować abstrakcyjnej klasy bazowej i podklas tej klasy bazowej, zbierać wskaźniki do wszystkich różnych typów w wektorze i wywoływać na nich metody. Dodatkowo, jeśli celem idiomu pimpl jest ukrycie szczegóły implementacji klasy to tylko prawie udaje się: sam wskaźnik jest szczegółem implementacji. Być może nieprzejrzysty szczegół implementacji. Ale jednak szczegóły realizacji.

Istnieje alternatywa dla idiomu pimpl, który może być użyty do usunięcia wszystkich szczegółów implementacji z interfejsu, zapewniając jednocześnie typ bazowy, który może być użyty polimorficznie, w razie potrzeby.

W pliku nagłówkowym DLL (ten #zawarty w kodzie klienta) Utwórz abstrakcyjna klasa z tylko publicznymi metodami i pojęciami, które dyktują, jak klasa ma być instancjowana (np. publiczne metody fabryczne i metody klonowania): {]}

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*
    };
};

...Animal jest abstrakcyjną klasą bazową i dlatego nie można utworzyć instancji; nie trzeba deklarować prywatnego ctor. Obecność Wirtualnego dtor zapewnia, że jeśli ktoś deletes Animal*, zostanie również wywołana odpowiednia podklasa' dtor.

W celu wdrożenia różnych podklas typ bazowy (np. psy i koty), Można zadeklarować klasy na poziomie implementacji w DLL. Klasy te wywodzą się ostatecznie z abstrakcyjnej klasy bazowej zadeklarowanej w pliku nagłówkowym, a metody fabryczne faktycznie utworzyłyby instancję jednej z tych podklas.

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";
}

Teraz masz interfejs zdefiniowany w nagłówku i szczegóły implementacji całkowicie oddzielone, gdzie kod klienta nie może go zobaczyć w ogóle. Użyłbyś tego wywołując metody zadeklarowane w pliku nagłówkowym z kodu, który łączy się z DLL. Oto przykładowy sterownik:

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;
}
 7
Author: John Dibling,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-22 21:53:49

Google "pimple idiom" lub " handle C++".

 3
Author: BubbaT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-22 19:34:54

Tak, to może być pożądana rzecz do zrobienia. Jednym z prostych sposobów jest to, aby Klasa implementacyjna wywodziła się z klasy zdefiniowanej w nagłówku.

Minusem jest to, że kompilator nie będzie wiedział, jak skonstruować klasę, więc będziesz potrzebował jakiejś metody fabrycznej, aby uzyskać instancje klasy. Nie będzie możliwe posiadanie lokalnych instancji na stosie.

 3
Author: Mark Ransom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-22 19:35:35

Musisz zadeklarować wszystkie elementy w nagłówku, aby kompilator wiedział, jak duży jest obiekt i tak dalej.

Ale możesz to rozwiązać za pomocą interfejsu:

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)
  {
    ...
  }
 2
Author: mmmmmmmm,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-23 22:19:01

Czy można zrobić nagłówek C++ plik (.h) deklaruje klasę, oraz jego metody publiczne, ale nie zadeklarować członków prywatnych w tym zajęcia?

Najbardziej najbliższą odpowiedzią jest idiom PIMPL.

Powered this The Fast Pimpl Idiom from Herb Sutter.

IMO Pimpl jest naprawdę przydatny na początkowych etapach rozwoju, gdzie Twój plik nagłówka będzie zmieniał się wiele razy. Pimpl ma swój koszt ze względu na alokację\dealokację wewnętrznych obiekt na stercie.

 0
Author: aJ.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-22 19:38:03

Sprawdź klasę The Handle-Body Idiom w C++

 -1
Author: J.W.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-22 19:38:00