Заголовочный файл С++, который объявляет класс и методы, но не члены?

Можно ли создать заголовочный файл C++ (.h), который объявляет класс и его общедоступные методы, но не определяет частные члены в этом классе? Я нашел несколько страниц, в которых говорится, что вы должны объявить класс и все его члены в заголовке. файл, затем определите методы отдельно в вашем файле cpp. Я спрашиваю, потому что хочу иметь класс, определенный в Win32 DLL, и я хочу, чтобы он был должным образом инкапсулирован: внутренняя реализация этого класса может измениться, включая его члены, но эти изменения не должны влиять на код, использующий класс. .

Я предполагаю, что если бы у меня было это, то компилятор не смог бы заранее узнать размер моих объектов. Но это должно быть хорошо, пока компилятор достаточно умен, чтобы использовать конструктор и просто передавать указатели на место в памяти, где хранится мой объект, и никогда не позволять мне запускать «sizeof (MyClass)».

Обновление: Спасибо всем, кто ответил! Кажется, что идиома pimpl — хороший способ добиться того, о чем я говорил. Я собираюсь сделать что-то подобное:

В моем файле Win32 DLL будет куча отдельных функций, например:

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

Это типичный способ, которым Microsoft пишет свои файлы DLL, поэтому я думаю, что для этого есть веская причина.

Но я хочу воспользоваться прекрасным синтаксисом C++ для классов, поэтому я напишу класс-оболочку для всех этих функций. У него будет один член, который будет "void * pimpl". Этот класс-оболочка будет настолько простым, что я мог бы просто объявить его И определить в заголовочном файле. Но, насколько я могу судить, у этого класса-оболочки нет иных целей, кроме как сделать код C++ красивым.


person David Grayson    schedule 22.04.2009    source источник


Ответы (8)


Я думаю, что вы ищете то, что называется "идиомой pimpl". Чтобы понять, как это работает, вам нужно понять, что в C++ вы можете предварительно объявить что-то вроде этого.

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

Таким образом, переадресация объявления означает обещание полностью объявить тип позже. В нем говорится: «Я обещаю, что будет эта штука под названием CWidget. Я расскажу вам об этом позже».

Правила предварительного объявления говорят, что вы можете определить указатель или ссылку на что-то, что было объявлено заранее. Это потому, что указатели и ссылки на самом деле являются просто адресами — числом, где будет находиться эта вещь, которую еще предстоит определить. Возможность объявить указатель на что-то, не объявляя его полностью, удобна по многим причинам.

Это полезно здесь, потому что вы можете использовать это, чтобы скрыть некоторые внутренние элементы класса, используя метод «pimpl». Pimpl означает «указатель на реализацию». Таким образом, вместо «виджета» у вас есть класс, который является фактической реализацией. Класс, который вы объявляете в своем заголовке, — это просто переход к классу CImpl. Вот как это работает:

// 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 методы CThing определены как сквозные для реализации:

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

тада! Вы скрыли все детали в своем cpp, и ваш заголовочный файл представляет собой очень аккуратный список методов. Это отличная вещь. Единственное отличие от приведенного выше шаблона — это то, что люди могут использовать boost::shared_ptr‹> или другой интеллектуальный указатель для реализации. Что-то, что удаляет себя.

Кроме того, имейте в виду, что этот метод сопряжен с некоторыми неприятностями. Отладка может быть немного раздражающей (дополнительный уровень перенаправления для пошагового выполнения). Это также много накладных расходов для создания класса. Если вы сделаете это для каждого класса, вы устанете печатать :).

person Doug T.    schedule 22.04.2009

Используйте идиому pimpl .

person bayda    schedule 22.04.2009

идиома pimpl добавляет в ваш класс закрытый элемент данных void*, и это полезная техника, если вам нужно что-то быстрое и грязное. Однако у него есть свои недостатки. Главным из них является то, что это затрудняет использование полиморфизма в абстрактном типе. Иногда вам может понадобиться абстрактный базовый класс и подклассы этого базового класса, собрать указатели на все различные типы в векторе и вызвать для них методы. Кроме того, если цель идиомы pimpl состоит в том, чтобы скрыть детали реализации класса, тогда это только почти преуспевает: сам указатель является деталью реализации. Непрозрачная деталь реализации, возможно. Но все же деталь реализации.

Существует альтернатива идиоме pimpl, которую можно использовать для удаления всех деталей реализации из интерфейса, предоставляя при этом базовый тип, который при необходимости можно использовать полиморфно.

В заголовочном файле вашей DLL (тот, который #included клиентским кодом) создайте абстрактный класс только с общедоступными методами и концепциями, которые определяют, как должен быть создан экземпляр класса (например, общедоступные фабричные методы и методы клонирования):

питомник.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 – это абстрактный базовый класс, поэтому его экземпляр не может быть создан. ; нет необходимости объявлять private ctor. Наличие виртуального dtor гарантирует, что если кто-то deletes Animal*, будет также вызван надлежащий dtor подкласса.

Чтобы реализовать различные подклассы базового типа (например, собак и кошек), вы должны объявить классы уровня реализации в вашей DLL. Эти классы, в конечном счете, являются производными от абстрактного базового класса, который вы объявили в своем заголовочном файле, и фабричные методы фактически будут создавать экземпляры одного из этих подклассов.

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

Теперь у вас есть интерфейс, определенный в заголовке, и детали реализации, полностью отделенные друг от друга, где клиентский код вообще их не видит. Вы могли бы использовать это, вызывая методы, объявленные в вашем заголовочном файле, из кода, который ссылается на вашу DLL. Вот пример драйвера:

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;
}
person John Dibling    schedule 22.04.2009
comment
Один нит — указатель pimpl не должен быть пустым*. Все, что нужно сделать, это чтобы публичный заголовок перенаправил ссылку на класс pimpl. - person Michael Burr; 23.04.2009

Погуглите "pimple idiom" или "handle C++".

person BubbaT    schedule 22.04.2009
comment
Лучше пишется «pimpl» (для «частной реализации»)? - person Jonathan Leffler; 22.04.2009

Да, это может быть желательным делом. Один из простых способов — сделать класс реализации производным от класса, определенного в заголовке.

Недостатком является то, что компилятор не будет знать, как построить ваш класс, поэтому вам понадобится какой-то фабричный метод для получения экземпляров класса. Невозможно иметь локальные экземпляры в стеке.

person Mark Ransom    schedule 22.04.2009

Вы должны объявить все элементы в заголовке, чтобы компилятор знал, насколько велик объект и так далее.

Но вы можете решить это с помощью интерфейса:

доб.ч:

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

инт.ч:

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

внутр.cpp:

  void ExtClassImpl::func1(int xy)
  {
    ...
  }
  int ExtClassImpl::func2(XYClass&param)
  {
    ...
  }
person mmmmmmmm    schedule 23.04.2009

Можно ли создать заголовочный файл C++ (.h), который объявляет класс и его общедоступные методы, но не объявляет частные члены в этом классе?

Самый близкий ответ - идиома PIMPL.

Обратитесь к этой идиоме Fast Pimpl от Херба Саттера.

IMO Pimpl действительно полезен на начальных этапах разработки, когда ваш заголовочный файл будет меняться много раз. Pimpl имеет свою стоимость из-за выделения\освобождения внутреннего объекта в куче.

person aJ.    schedule 22.04.2009

Ознакомьтесь с классом The Handle-Body Идиома в С++

person J.W.    schedule 22.04.2009