Скрыть определение приватных переменных класса
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.
Мне показалась тема интересной. Для обсуждения сделал топик на форуме. Для начала предыстория вопроса:
Вопрос 1. По сути, Иван в п.2 «на пальцах» описал pimpl? Или я что-то не так понял?
Вопрос 2. Какие есть плюсы и минусы у способа с наследованием и pimpl?
Да.
В первом случае, все функции-члены должны быть виртуальными, иначе получим здоровую кашу. Виртуальность принесет с собой накладные расходы на каждый вызов функций-членов. Убиваем возможность инлайна практически наглухо.
Во втором случае, все обращения будут построены через указатель, значит имеем такие же расходы, как и от наследования. Плюс ко всему необходимо будет держать где-то буфер с самими данными, например, в динамической памяти, а это тоже уже классные проблемы. Конструкторы, деструкторы и операторы присваивания, которые генерирует компилятор уже не подойдут, нужно будет писать свои. Если компилятор не поддерживает межмодульное встраивание функций, то получим невозможность инлайна геттеров, сеттеров и других функций. Зато такой объект можно быстро переместить — всего лишь поменять один указатель в каждом из объектов, а если использовать FastPimpl из статьи, то это убьет эту возможность. Раздумаем объект на один лишний указатель. Несколько затруднительно искать несовместимости ABI, т.к. внешне у нас всё аналогично, да и для раздельной компиляции может быть сюрприз в рантайме. :)
Ни то ни другое не применимо с шаблонами.
Думаю, здесь можно много чего написать еще.
Т.е., если я правильно вас понял, способ с наследованием предпочтительнее.
Что такое «межмодульное встраивание функций»?
Не понял. Если можно, поподробнее.
Для начала нужно определиться, нужно ли что-то скрывать и зачем (к слову, если нужно, то данные эти всё равно получат, исследовав память). В большинстве случаев такой необходимости нет. Способ с наследованием предполагает, как правило, именно полиморфное использование класса, а не ради сокрытия. И необходимо понимать, что доступа к классу-наследнику у клиента всё равно не будет, у него будет только интерфейс базового класса, т.к. мы скрыли класс-наследник. А значит мы не сможем создать на стеке данный объект, а также нам нужна будет фабрика, которая может делать объект нашего класса-наследника на куче (хотя можно накостылить сомнительных решений). И здесь тоже свои запары. Нужно точно определить как и чем выделялась память, чтобы её корректно освободить. Это значит, что помимо функции-фабрики нам понадобиться еще и своя функция удаления объекта, которую мы должны будем предоставить вместе с базовым интерфейсом. Посему использовать умные указатели с такого рода объектами нельзя без указания своей функции удаления, а это уже вносит еще расходов (либо на создание своего указателя под конкретные типы, либо стандартный unique_ptr с разбухшим размером, но мы всё равно лишаемся функций вида make_unique). Соответственно, нам нужно будет более тщательно следить за памятью при таком подходе.
Это значит, что функция, может быть встроена компилятором, вместо её вызова, даже если она находится в другой единице трансляции.
Достаточно сложно притянуть за уши какой-то мелкую поделку для демонстрации. Вообще с++ и так переносим только на уровне исходников, так мы еще и всё специально всё скрыли. Но можно ограничиться хотя бы тем, что в константных функциях-членах мы можем менять объект с реализацией. :)
Не знаю, что хотел скрывать человек, с поста которого всё началось. Но я, например, столкнулся с подобной проблемой при переносе кое-каких своих программ с винды на линукс. Собственно перенос был сделан без этих заморочек (просто исправлением исходников) — там других проблем хватило. А потом вот задумался, а как это сделать, что бы был единый комплект исходников, который можно было компилить и под винду и под линукс.
Проблема в том, что класс, реализующий системозависимые вещи, должен иметь разный набор переменных-членов и, соответственно, различные реализации методов. Но при этом открытый интерфейс этого класса должен быть одинаковым для разных ОС. Т.е. что бы при компиляции для остальной программы не было разницы какая из версий класса используется: виндовая или линуксовая.
Если делаем «в лоб», то для каждого такого класса получаем для каждой ОС пару файлов заголовок + реализация. Причем в заголовках открытые члены (данные и функции) должны точно совпадать. Отсюда следует, что хотелось бы иметь один заголовок и две реализации, что бы гарантировать согласованность интерфейса.
Что посоветуете?
Стандартная практика — делаем обертки для платформозависимых вещей и настраиваем как это должно всё собираться в результате. Никаких двух заголовков и т.д. Части остальной программы общаются только с обертками. Тогда функционал внутри можно реализовать как угодно. Можно pimpl, можно без него. Всё что не должно компилироваться под заданную платформу можно убить во время компиляции. И внутри оберток может отличаться что угодно.
Это значит, что код прибит к винде гвоздями. Если писать под несколько платформ, то нужно это делать сразу. Ни одна часть, кроме оберток не должна общаться с платформозависимыми вещами, иначе придется напихать всяких препроцессорных макросов дополнительно.
Собственно у меня так и сделано. Платформозависимые вещи убраны в класс. Вопрос в том, как лучше чисто технически организовать этот класс.
Условная компиляция через препроцессор? Был у меня такой вариант. Жуть и каша. С кодом сложно работать из-за большого объема.
Сейчас я разделил реализации класса для разных осей по подкаталогам и подключаю нужный на уровне управления проектом. Что тоже не айс, но всё же лучше.
Не так. Платформозависимые вещи убраны в класс. Я просто отредактировал класс для другой ОС. И в итоге получил ту самую проблему, о которой написал выше: фактически два различных класса (каждый под свою ОС), открытый интерфейс которых должен в точности совпадать.
Да, можно в системе сборки можно сделать нечто подобное:
У меня сейчас в одном из проектов так сторонняя библиотека цепляется.
Это и называется «прибит гвоздями». Платформозависимые вещи торчат наружу (хоть и не в public, но в интерфейсе класса). Как вариант — тот же базовый интерфейс с виртуальными функциями, реализация же своя для каждой ОС. Также можно использовать PImpl, но это тоже породит несколько файлов реализации. Это вообще, наверное, неизбежно, если не делать на препроцессоре. Однако, мы можем предоставить библиотеку, скрывающую всеплатформозависимые вещи. Эта библиотека как раз и должна знать, как ей собираться под заданную платформу. Наружу предоставляем только функции, которые оперируют уже обернутыми данными и функции для создания этих самых обернутых данных. И даже это врядли спасет от препроцессора. Платформы имеют столько различий, что диву даешься и это я сейчас не о различиях Windows и Linux, а даже просто о разных версиях Linux, разных версиях Windows, MacOS и т.д.
Лично я люблю экспортировать из библиотек только функции. Функции эти, если нужно, создают нужные объекты. Наружу можно пропустить как интерфейс класса (либо с виртуальными функциями, либо с pimpl, etc), либо некий
блин )))
* либо некий хендлер, устройство которого знает только библиотека. Например, это будет указатель на void, который будет приниматься/возвращаться функциями и никто кроме библиотеки в принципе не знает о его устройстве (стандартная киллер-фича C — хрен разберешь что там под капотом ))). Решение с pimpl тоже хорошо подойдет.
Ну а наш класс-обертка работает примерно так:
То есть класс просто оборачивает предоставленный интерфейс. В принципе, работает также, как и pimpl, но я люблю именно такие обертки.
Не-е-е... «прибит гвоздыми» — это когда системозависимые вещи ровным слоем разбросаны по всем исходникам ))))
Несколько файлов реализации — не страшно. Это лучше, чем один большой файл с кучей директив условной трансляции. Главное, что бы заголовок был один.
Богохульничаете? А как же строгая типизация? А как же RAII? ;)
Я не совсем понял вашу реализацию с библиотекой.
Сначала вы пишите несколько версий библиотеки под разные ОС (с классами, RAII, строгой типизацией и другими примочками C++). Потом грохаете всю эту красоту общим С-шным интерфейсом для библиотеки. А потом типа восстанавливаете объектный интерфейс оборачивая библиотечные функции еще одним классом? А
class mylib_data_ptr
— это по функционалу что-то типаshared_ptr
? Я правильно понял? Как-то вроде слишком сложно получается.Да, оно. Хотел же RAII, вот это оно и есть.
Во-первых, под капотом не обязан быть класс. Указатель вообще можно сделать void. И там вообще может быть не C++ внутри.
В ином случае код не будет переносим между компиляторами.
такой код в большей степени переносим в бинарном виде между компиляторами.
Классы же, экспортируемые из собранной библиотеки делают код непереносимым, окромя пустых с чисто-виртуальными функциями.
Это худо-бедно, но переносимо в бинарном виде.
А теперь внимание
class Some {
virtual void work() = 0;
virtual ~Some();//добавили виртуальный деструктор
};
код стал непереносим. Добро пожаловать в мир C++. )))
Кстати, код у шаблонной оберткой в возвращаемом значении тоже будет непереносим. Нужно будет делать что-то вроде:
То есть создание враппера перенесли на сторону клиента, чтобы наша библиотека не зависила от компилятора.
bugfix
extern «C» char mylib_load_data(Handle );
мда... markdown то еще говнецо...
Вы тоже это заметили?
Трудно не заметить то, что воняет. )))
У меня пока до самостоятельно живущей бинарной библиотеки дело не дошло.
Всё на уровне исходного кода.
Спасибо за ответ. Наверное буду использовать все-таки pimpl.
Здешний форум вообще странненько сделан. Я не силен в сайтостроении, но есть же и нормальные форумные движки с древовидной системой сообщений, и компоненты для WYSIWYG-редактирования сообщений, и компоненты для редактирования/отображения исходного кода. Почему не использовать?
Тем более, что на грабли markdown-а наступают ежедневно. Я иногда смотрю вопросы от совсем начинающих — сообщения так выглядят, что даже не то что отвечать не хочется, разбираться в этом месиве из кода желания нет ((
Пожалуйста. Но относитесь это как к дополнительной информации. Конечный выбор решения должен основываться на конкретных задачах.