Скрыть определение приватных переменных класса

Мне показалась тема интересной. Для обсуждения сделал топик на форуме. Для начала предыстория вопроса:


Иван 14 июня 2017

А как быть если хочется скрыть приватное определение переменных от глаз тех кто будет использовать код ?

Навскидку два способа.

  1. Сделать абстрактный класс от которого унаследовать класс с реализацией. Абстрактный класс — доступен для просмотра, но ничего не делает и данных не содержит. Только интерфейс. Класс с реализацией, который содержит данные и реализации методов, никому не показывать.

  2. Сделать класс-обёртку. В качестве данных только указатель на класс с реализацией. В конструкторе создавать экземпляр класса с реализацией, в деструкторе — уничтожать. В методах — вызывать методы класса-реализации через указатель.


MasterOfAlteran 14 июня 2017

Идиома pImpl: https://habrahabr.ru/post/111602/


Вопрос 1. По сути, Иван в п.2 «на пальцах» описал pimpl? Или я что-то не так понял?

Вопрос 2. Какие есть плюсы и минусы у способа с наследованием и pimpl?

По сути, Иван в п.2 «на пальцах» описал pimpl?

Да.

Какие есть плюсы и минусы у способа с наследованием и pimpl?

В первом случае, все функции-члены должны быть виртуальными, иначе получим здоровую кашу. Виртуальность принесет с собой накладные расходы на каждый вызов функций-членов. Убиваем возможность инлайна практически наглухо.

Во втором случае, все обращения будут построены через указатель, значит имеем такие же расходы, как и от наследования. Плюс ко всему необходимо будет держать где-то буфер с самими данными, например, в динамической памяти, а это тоже уже классные проблемы. Конструкторы, деструкторы и операторы присваивания, которые генерирует компилятор уже не подойдут, нужно будет писать свои. Если компилятор не поддерживает межмодульное встраивание функций, то получим невозможность инлайна геттеров, сеттеров и других функций. Зато такой объект можно быстро переместить — всего лишь поменять один указатель в каждом из объектов, а если использовать FastPimpl из статьи, то это убьет эту возможность. Раздумаем объект на один лишний указатель. Несколько затруднительно искать несовместимости ABI, т.к. внешне у нас всё аналогично, да и для раздельной компиляции может быть сюрприз в рантайме. :)

Ни то ни другое не применимо с шаблонами.

Думаю, здесь можно много чего написать еще.

Т.е., если я правильно вас понял, способ с наследованием предпочтительнее.

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

Что такое «межмодульное встраивание функций»?

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

Не понял. Если можно, поподробнее.

Т.е., если я правильно вас понял, способ с наследованием предпочтительнее.

Для начала нужно определиться, нужно ли что-то скрывать и зачем (к слову, если нужно, то данные эти всё равно получат, исследовав память). В большинстве случаев такой необходимости нет. Способ с наследованием предполагает, как правило, именно полиморфное использование класса, а не ради сокрытия. И необходимо понимать, что доступа к классу-наследнику у клиента всё равно не будет, у него будет только интерфейс базового класса, т.к. мы скрыли класс-наследник. А значит мы не сможем создать на стеке данный объект, а также нам нужна будет фабрика, которая может делать объект нашего класса-наследника на куче (хотя можно накостылить сомнительных решений). И здесь тоже свои запары. Нужно точно определить как и чем выделялась память, чтобы её корректно освободить. Это значит, что помимо функции-фабрики нам понадобиться еще и своя функция удаления объекта, которую мы должны будем предоставить вместе с базовым интерфейсом. Посему использовать умные указатели с такого рода объектами нельзя без указания своей функции удаления, а это уже вносит еще расходов (либо на создание своего указателя под конкретные типы, либо стандартный unique_ptr с разбухшим размером, но мы всё равно лишаемся функций вида make_unique). Соответственно, нам нужно будет более тщательно следить за памятью при таком подходе.

Что такое «межмодульное встраивание функций»?

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

Не понял. Если можно, поподробнее.

Достаточно сложно притянуть за уши какой-то мелкую поделку для демонстрации. Вообще с++ и так переносим только на уровне исходников, так мы еще и всё специально всё скрыли. Но можно ограничиться хотя бы тем, что в константных функциях-членах мы можем менять объект с реализацией. :)

Для начала нужно определиться, нужно ли что-то скрывать и зачем

Не знаю, что хотел скрывать человек, с поста которого всё началось. Но я, например, столкнулся с подобной проблемой при переносе кое-каких своих программ с винды на линукс. Собственно перенос был сделан без этих заморочек (просто исправлением исходников) — там других проблем хватило. А потом вот задумался, а как это сделать, что бы был единый комплект исходников, который можно было компилить и под винду и под линукс.

Проблема в том, что класс, реализующий системозависимые вещи, должен иметь разный набор переменных-членов и, соответственно, различные реализации методов. Но при этом открытый интерфейс этого класса должен быть одинаковым для разных ОС. Т.е. что бы при компиляции для остальной программы не было разницы какая из версий класса используется: виндовая или линуксовая.

Если делаем «в лоб», то для каждого такого класса получаем для каждой ОС пару файлов заголовок + реализация. Причем в заголовках открытые члены (данные и функции) должны точно совпадать. Отсюда следует, что хотелось бы иметь один заголовок и две реализации, что бы гарантировать согласованность интерфейса.

Что посоветуете?

Что посоветуете?

Стандартная практика — делаем обертки для платформозависимых вещей и настраиваем как это должно всё собираться в результате. Никаких двух заголовков и т.д. Части остальной программы общаются только с обертками. Тогда функционал внутри можно реализовать как угодно. Можно pimpl, можно без него. Всё что не должно компилироваться под заданную платформу можно убить во время компиляции. И внутри оберток может отличаться что угодно.

просто исправлением исходников

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

Стандартная практика — делаем обертки для платформозависимых вещей

Собственно у меня так и сделано. Платформозависимые вещи убраны в класс. Вопрос в том, как лучше чисто технически организовать этот класс.

Всё что не должно компилироваться под заданную платформу можно убить во время компиляции.

Условная компиляция через препроцессор? Был у меня такой вариант. Жуть и каша. С кодом сложно работать из-за большого объема.

Сейчас я разделил реализации класса для разных осей по подкаталогам и подключаю нужный на уровне управления проектом. Что тоже не айс, но всё же лучше.

Это значит, что код прибит к винде гвоздями.

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

Сейчас я разделил реализации класса для разных осей по подкаталогам и подключаю нужный на уровне управления проектом.

Да, можно в системе сборки можно сделать нечто подобное:

PATH_TO_IMPL=/windows/
SOURCES = PATH_TO_IMPL *.cpp
HEADERS = PATH_TO_IMPL *.hpp

У меня сейчас в одном из проектов так сторонняя библиотека цепляется.

Я просто отредактировал класс для другой ОС

Это и называется «прибит гвоздями». Платформозависимые вещи торчат наружу (хоть и не в public, но в интерфейсе класса). Как вариант — тот же базовый интерфейс с виртуальными функциями, реализация же своя для каждой ОС. Также можно использовать PImpl, но это тоже породит несколько файлов реализации. Это вообще, наверное, неизбежно, если не делать на препроцессоре. Однако, мы можем предоставить библиотеку, скрывающую всеплатформозависимые вещи. Эта библиотека как раз и должна знать, как ей собираться под заданную платформу. Наружу предоставляем только функции, которые оперируют уже обернутыми данными и функции для создания этих самых обернутых данных. И даже это врядли спасет от препроцессора. Платформы имеют столько различий, что диву даешься и это я сейчас не о различиях Windows и Linux, а даже просто о разных версиях Linux, разных версиях Windows, MacOS и т.д.
Лично я люблю экспортировать из библиотек только функции. Функции эти, если нужно, создают нужные объекты. Наружу можно пропустить как интерфейс класса (либо с виртуальными функциями, либо с pimpl, etc), либо некий

блин )))
* либо некий хендлер, устройство которого знает только библиотека. Например, это будет указатель на void, который будет приниматься/возвращаться функциями и никто кроме библиотеки в принципе не знает о его устройстве (стандартная киллер-фича C — хрен разберешь что там под капотом ))). Решение с pimpl тоже хорошо подойдет.

Ну а наш класс-обертка работает примерно так:

//в хедере библиотеки:
//class HandlerType;

template<typename T>
class mylib_data_ptr
{
//реализует RAII
};


extern "C" HandlerType *mylib_create(/*params*/);
extern "C" void mylib_destroy(HandlerType *);
extern "C" mylib_data_ptr<T> mylib_load_data(HandlerType *, /*params*/);
extern "C" void mylib_delete(void *);
//...

class MyClass
{
public:
   MyClass(/*params*/) : m_handler(mylib_create(/*params*/)){
   }
   //...
   std::string load_data(/*params*/) {
      return {mylib_load_data(m_handler, /*params*/)};
   }
   //...
   ~MyClass() {
      mylib_destroy(m_handler);
   }
private:
   HandlerType *m_handler;
};

То есть класс просто оборачивает предоставленный интерфейс. В принципе, работает также, как и pimpl, но я люблю именно такие обертки.

Это и называется «прибит гвоздями».

Не-е-е... «прибит гвоздыми» — это когда системозависимые вещи ровным слоем разбросаны по всем исходникам ))))

но это тоже породит несколько файлов реализации

Несколько файлов реализации — не страшно. Это лучше, чем один большой файл с кучей директив условной трансляции. Главное, что бы заголовок был один.

Лично я люблю экспортировать из библиотек только функции. Функции эти, если нужно, создают нужные объекты...
некий хендлер, устройство которого знает только библиотека. Например, это будет указатель на void

Богохульничаете? А как же строгая типизация? А как же RAII? ;)

Я не совсем понял вашу реализацию с библиотекой.

Сначала вы пишите несколько версий библиотеки под разные ОС (с классами, RAII, строгой типизацией и другими примочками C++). Потом грохаете всю эту красоту общим С-шным интерфейсом для библиотеки. А потом типа восстанавливаете объектный интерфейс оборачивая библиотечные функции еще одним классом? А class mylib_data_ptr — это по функционалу что-то типа shared_ptr? Я правильно понял? Как-то вроде слишком сложно получается.

А class mylib_data_ptr — это по функционалу что-то типа shared_ptr? Я правильно понял? Как-то вроде слишком сложно получается.

Да, оно. Хотел же RAII, вот это оно и есть.

Сначала вы пишите несколько версий библиотеки под разные ОС (с классами, RAII, строгой типизацией и другими примочками C++). Потом грохаете всю эту красоту общим С-шным интерфейсом для библиотеки

Во-первых, под капотом не обязан быть класс. Указатель вообще можно сделать void. И там вообще может быть не C++ внутри.

А потом типа восстанавливаете объектный интерфейс оборачивая библиотечные функции еще одним классом?

В ином случае код не будет переносим между компиляторами.

extern "C" Handle *create();
extern "C" void release(Handle *);
extern "C" void work(Handle *);

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

class Some {
    virtual void work() = 0;
};

extern "C" Some *create();
extern "C" void release(Some *);

Это худо-бедно, но переносимо в бинарном виде.
А теперь внимание
class Some {
virtual void work() = 0;
virtual ~Some();//добавили виртуальный деструктор
};

код стал непереносим. Добро пожаловать в мир C++. )))

Кстати, код у шаблонной оберткой в возвращаемом значении тоже будет непереносим. Нужно будет делать что-то вроде:

extern "C" char *load_data(Handle *);

inline ptr_wrapper<char*> load_data(Handle *handle)
{
    return {mylib_load_data(handle)};
}

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

bugfix

extern «C» char mylib_load_data(Handle );

мда... markdown то еще говнецо...

мда... markdown то еще говнецо...

Вы тоже это заметили?

Вы тоже это заметили?

Трудно не заметить то, что воняет. )))

Это худо-бедно, но переносимо в бинарном виде.

У меня пока до самостоятельно живущей бинарной библиотеки дело не дошло.
Всё на уровне исходного кода.

Спасибо за ответ. Наверное буду использовать все-таки pimpl.

мда... markdown то еще говнецо...

Здешний форум вообще странненько сделан. Я не силен в сайтостроении, но есть же и нормальные форумные движки с древовидной системой сообщений, и компоненты для WYSIWYG-редактирования сообщений, и компоненты для редактирования/отображения исходного кода. Почему не использовать?

Тем более, что на грабли markdown-а наступают ежедневно. Я иногда смотрю вопросы от совсем начинающих — сообщения так выглядят, что даже не то что отвечать не хочется, разбираться в этом месиве из кода желания нет ((

Спасибо за ответ. Наверное буду использовать все-таки pimpl.

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

Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.

Ответить

Вы можете использовать разметку markdown для оформления комментариев и постов. Используйте функцию предпросмотра для проверки корректности разметки.

Пожалуйста, оформляйте исходный код в соответствии с правилами разметки. Для того, чтобы вставить код в комментарий, скопируйте его в текстовое поле ниже, после чего выделите то, что скопировали и нажмите кнопку «код» в панели инструментов. Иначе ваш код может принять нечитаемый вид.

Либо производите оформление кода вручную, следующим образом:

``` #include <iostream> using namespace std; int main() { // ... } ```

Предпросмотр сообщения

Ваше сообщение пусто.