Заголовочный файл C ++, который объявляет класс и методы, но не члены? - PullRequest
19 голосов
/ 22 апреля 2009

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

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

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

Мой файл DLL Win32 будет иметь несколько отдельных функций, таких как:

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

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

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

Ответы [ 8 ]

33 голосов
/ 22 апреля 2009

Я думаю, что вы ищете то, что называется "идиома 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, определенные как переходы к 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();
}

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

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

13 голосов
/ 22 апреля 2009

Использование pimpl идиома.

7 голосов
/ 23 апреля 2009

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

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

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

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

... Животное является абстрактным базовым классом и поэтому не может быть создано; никакой частный ctor не должен быть объявлен. Наличие виртуального dtor гарантирует, что, если кто-то delete s Animal*, будет также вызван соответствующий dtor подкласса.

Чтобы реализовать различные подклассы базового типа (например, dogs & cats), вы должны объявить классы уровня реализации в вашей 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;
}
3 голосов
/ 22 апреля 2009

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

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

3 голосов
/ 22 апреля 2009

Google "идиома прыщ" или "обрабатывать C ++".

2 голосов
/ 24 апреля 2009

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

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

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)
  {
    ...
  }
0 голосов
/ 22 апреля 2009

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

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

См. Это Быстрая идиома Pimpl от Херб Саттер.

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

0 голосов
/ 22 апреля 2009

Проверьте класс Тело ручки Идиома в C ++

...