Подтвердить что ты не робот

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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 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 < > или другой умный указатель для имплантов. Что-то, что удаляет себя.

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

Ответ 2

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

Ответ 3

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

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

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

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

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

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 own properties/behavior (speak)
//      Each instance has it 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;
}

Ответ 4

Google "pimple idiom" или "handle С++".

Ответ 5

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

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

Ответ 6

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

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

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)
  {
    ...
  }

Ответ 7

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

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

Обратитесь к Идиома Fast Pimpl от Herb Sutter.

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

Ответ 8

Посмотрите класс The Handle-Body Идиома в С++