2013-07-25 2 views
1

Инкапсуляция (скрытие информации) является очень полезной концепцией, гарантирующей, что в API класса будут опубликованы только самые минимальные детали.Как вы делаете «истинную» инкапсуляцию в C++?

Но я не могу не думать о том, что способ C++ делает это немного недостаточным. Возьмем, к примеру, (по Цельсию на основе) Температурный класс как:

class tTemp { 
    private: 
     double temp; 
     double tempF (double); 
    public: 
     tTemp(); 
     ~tTemp(); 
     setTemp (double); 
     double getTemp(); 
     double getTempF(); 
}; 

Теперь, это очень простой случай, но он иллюстрирует точку, что инкапсуляция не является совершенным. «Реал» инкапсуляция бы скрыть все ненужной информации, такой как:

  • тот факт, что данные сохраняются внутри в temp переменной (и его тип).
  • факт, что существует внутренняя процедура преобразования Фаренгейта/Цельсия.

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

Не поймите меня неправильно, я не критикую C++, так как он соответствует заявленной цели предотвращения клиентов от с использованием частных битов, но для более сложных классов вы можете легко разработать внутренние детали на основе имена, типы и подписи частных данных и функций.

Как C++ позволить разработчикам, чтобы скрыть эту информацию (предполагается, что is Возможно)? В C я просто использовал непрозрачный тип, чтобы внутренние детали были скрыты, но как бы вы это сделали на C++?

Я полагаю, я мог бы поддерживать отдельный класс, полностью скрытый от клиента и известен только мой собственный код, а затем сохранить его экземпляр с void * в видимом классе (литье в моем коде), но это кажется довольно болезненным процессом. Есть ли более простой способ в C++ достичь той же цели?

+0

Не могу дождаться, пока модули разобрались :) – chris

+1

Кто-то уже упоминал Pimpl, о котором вы более или менее косвенно упоминаете, но с большей безопасностью, чем void *. Другой подход заключается в объявлении класса интерфейса (сигнатуры являются чисто виртуальными) и фабрики, которая является единственной, которая посвящена внутренней работе конкретных производных классов. –

+0

То, что вы называете «истинным» инкапсуляцией, возможно * на C++, поскольку многие ответы * pimpl * подтверждают, но это связано с затратами. Скрытие внутренних компонентов от * пользователей * хорошо и хорошо, но их скрытие от компилятора * блокирует оптимизацию. Это основная цель C++, которую производительность выигрывает, когда есть конфликт между производительностью и некоторыми другими функциями. – Casey

ответ

8

C++ использует идиому, известную как «pimpl» (частная реализация/указатель на реализацию), чтобы скрыть детали реализации. Взгляните на this MSDN article.

Одним словом, вы открываете свой интерфейс в файле заголовка, как обычно. Давайте использовать свой код в качестве примера:

tTemp.h

class tTemp { 
    private: 
     class ttemp_impl; // forward declare the implementation class 
     std::unique_ptr<ttemp_impl> pimpl; 
    public: 
     tTemp(); 
     ~tTemp(); 
     setTemp (double); 
     double getTemp (void); 
     double getTempF (void); 
}; 

Общедоступный интерфейс остается, но частные Внутренности были заменены с помощью смарт-указатель на частный класс реализации. Этот класс реализации находится только в соответствующем файле .cpp заголовка, он не раскрывается публично.

tTemp.cpp

class tTemp::ttemp_impl 
{ 
    // put your implementation details here 
} 

// use the pimpl as necessary from the public interface 
// be sure to initialize the pimpl! 
tTtemp::tTemp() : pimpl(new ttemp_impl) {} 

Это также имеет дополнительное преимущество, что позволяет изменять внутренности своего класса, не меняя заголовок, что означает меньше перекомпиляции для пользователей вашего класса.


Для полного решения, как показано в пре-C++ 11 ответа paxdiablo, но с unique_ptr вместо void *, вы можете использовать следующее. Первый ttemp.h:

#include <memory> 
class tTemp { 
public: 
    tTemp(); 
    ~tTemp(); 
    void setTemp(double); 
    double getTemp (void); 
    double getTempF (void); 

private: 
    class impl; 
    std::unique_ptr<impl> pimpl; 
}; 

Далее, "скрытый" реализация в ttemp.cpp:

#include "ttemp.h" 

struct tTemp::impl { 
    double temp; 
    impl() { temp = 0; }; 
    double tempF (void) { return temp * 9/5 + 32; }; 
}; 

tTemp::tTemp() : pimpl (new tTemp::impl()) {}; 

tTemp::~tTemp() {} 

void tTemp::setTemp (double t) { pimpl->temp = t; } 

double tTemp::getTemp (void) { return pimpl->temp; } 

double tTemp::getTempF (void) { return pimpl->tempF(); } 

И, наконец, ttemp_test.cpp:

#include <iostream> 
#include <cstdlib> 
#include "ttemp.h" 

int main (void) { 
    tTemp t; 
    std::cout << t.getTemp() << "C is " << t.getTempF() << "F\n"; 
    return 0; 
} 

И, как решение paxdiablo, в выход:

0C is 32F 

с дополнительным преимуществом более безопасного типа. Этот ответ является идеальным решением для C++ 11, см. Ответ paxdiablo, если ваш компилятор является pre-C++ 11.

+0

Так что вам не нужно 'ttemp_impl' _defined_ в видимом коде, так как вы храните указатель на объявленный вперед (непрозрачный) класс. Ницца. Я собираюсь в коде реализации, я бы полностью определил 'ttemp_impl' перед' tTemp' (следовательно, игнорируя объявление fwd), чтобы у меня был доступ к внутренним. И данные/функции должны быть публичными в 'ttemp_impl', чтобы я мог их назвать, да? Например, 'pimpl-> temp',' far = pimpl-> cToF (temp); 'и т. Д. – paxdiablo

+0

@paxdiablo Право. Класс impl должен быть определен до того, как он будет использоваться, и что-то, что должен знать публичный класс, должно быть общедоступным. На практике большинство публичных функций в конечном итоге станут тонкой оболочкой, которая продвигает функции impl. – drwwlkr

+0

Я не могу для жизни меня получить это, чтобы скомпилировать без страшного «ISO C++ запрещает объявление« unique_ptr »без ошибки типа, даже с объявлением вперед. Не уверен, что это функция C++ 11, которую вы используете здесь, но мне приходилось прибегать к 'void *', чтобы заставить ее работать. – paxdiablo

0

Частная реализация (PIMPL) - это способ, которым C++ может предоставить эту функцию. Поскольку у меня возникли проблемы с выбором unique_ptr для компиляции с CygWin g ++ 4.3.4, другой способ сделать это - использовать void * в вашем видимом классе следующим образом. Это позволит вам использовать компиляторы pre-C++ 11 и компиляторы, подобные вышеупомянутому gcc, который имел только экспериментальную поддержку для C++ 11.

Во-первых, заголовочный файл ttemp.h, который включает клиент. Это непрозрачно отражает внутреннюю структуру реализации, так что эти внутренние элементы полностью скрыты. Вы можете видеть, что единственная деталь показал это имя внутреннего класса и переменной, ни один из которых нужно раскрыть какую-либо информацию о том, как работа внутренних органов:

struct tTempImpl; 
class tTemp { 
public: 
    tTemp(); 
    ~tTemp(); 
    tTemp (const tTemp&); 
    void setTemp(double); 
    double getTemp (void); 
    double getTempF (void); 
private: 
    tTempImpl *pimpl; 
}; 

Далее, файл ttemp.cpp реализации которых и декларирует и определяет непрозрачный материал, а также определяет видимые пользователем детали. Поскольку пользователь не видит этот код, они не знают о том, как это реализовано:

#include "ttemp.h" 

struct tTempImpl { 
    double temp; 
    tTempImpl() { temp = 0; }; 
    double tempF (void) { return temp * 9/5 + 32; }; 
}; 

tTemp::tTemp() : pimpl (new tTempImpl()) { 
}; 

tTemp::~tTemp() { 
    delete pimpl; 
} 

tTemp::tTemp (const tTemp& orig) { 
    pimpl = new tTempImpl; 
    pimpl->temp = orig.pimpl->temp; 
} 

void tTemp::setTemp (double t) { 
    pimpl->temp = t; 
} 

double tTemp::getTemp (void) { 
    return pimpl->temp; 
} 

double tTemp::getTempF (void) { 
    return pimpl->tempF(); 
} 

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

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

Наконец, тестовая программа, показывающая, как все это висит вместе:

#include <iostream> 
#include "ttemp.h" 

int main (void) { 
    tTemp t; 
    std::cout << t.getTemp() << "C is " << t.getTempF() << "F\n"; 
    return 0; 
} 

Выход этого кода будучи, конечно:

0C is 32F 
4

Мысль я бы конкретизировать «класс интерфейса/factory ", о которых Дон Уэйкфилд упоминает в своем комментарии.Начнем с того, мы абстрагироваться от всех деталей реализации от интерфейса и определить абстрактный класс, который содержит только интерфейс к Temp:

// in interface.h: 
class Temp { 
    public: 
     virtual ~Temp() {} 
     virtual void setTemp(double) = 0; 
     virtual double getTemp() const = 0; 
     virtual double getTempF() const = 0; 

     static std::unique_ptr<Temp> factory(); 
}; 

Клиенты, которые хотят Temp объект позвонить на завод, чтобы построить один. Фабрика может предоставить некоторую сложную инфраструктуру, которая возвращает различные реализации интерфейса в разных условиях или что-то простое, как фабрика «просто дайте мне Temp» в этом примере.

Это возможно для классов реализации реализовать интерфейс, предоставляя переопределения для всех чистых деклараций виртуальных функций:

// in implementation.cpp: 
class ConcreteTemp : public Temp { 
    private: 
     double temp; 
     static double tempF(double t) { return t * (9.0/5) + 32; } 
    public: 
     ConcreteTemp() : temp() {} 
     void setTemp(double t) { temp = t; } 
     double getTemp() const { return temp; } 
     double getTempF() const { return tempF(temp); } 
}; 

и где-то (возможно, в том же implementation.cpp), нужно определить фабрику:

std::unique_ptr<Temp> Temp::factory() { 
    return std::unique_ptr<Temp>(new ConcreteTemp); 
} 

Этот подход немного более легко расширяемый, чем pimpl: любой, кто хочет реализовать интерфейс Temp, вместо того, чтобы существовать только одна «секретная» реализация. Там также немного меньше шаблонов, поскольку он использует встроенные механизмы языка для виртуальной диспетчеризации для отправки вызовов функций интерфейса в реализации.

+2

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

Смежные вопросы