Author Archives: elwood
Сегодня столкнулся с интересной проблемой и нашел не менее интересный способ решения. Как всем известно, если мы имеем иерархию классов class Derived : public Base, и шаблонный класс наподобие Template<typename T>, то классы, которые будут сгенерированы при инстанцировании шаблонов Template<Base> и Template<Derived> не имеют ничего общего. То есть мы не сможем привести тип Template<Derived> к типу Template<Base>, но ведь часто это просто необходимо ! В данном случае не работает ни один из способов приведения (даже reinterpret_cast). Приходится действовать обходным путем : брать адрес у объекта, приводить его к типу указателя на объект искомого типа, и выполнять разыменование. Выглядит это, мягко говоря, не слишком элегантно :
Template<Derived> derived; Template<Base>& base = *static_cast<Template<Base>*> (static_cast<void*> (&derived)); |
В принципе, таким способом можно выполнить приведение Template<Derived> к шаблону с любым аргументом-типом, главное следить, чтобы приведенный экземпляр вел себя корректно (вызывал правильный деструктор, обладал той же бинарной структурой итд). Как можно усовершенствовать этот код ? Идею подсказал мне коллега, напомнив про имеющуюся в арсенале С++ возможность перегрузки оператора приведения типов. В самом деле, почему нет ? Ведь мы можем определить оператор преобразования типа, и к тому же сделать его шаблонной функцией, которая будет принимать тип, в который необходимо выполнить преобразование. С учетом того, что для шаблонных функций работает механизм автоматического вывода аргументов шаблона, это решение кажется все более изящным:
template<typename T> class Template { public: template <typename U> operator Template<U>& () { return (*static_cast<Template<U>*> (static_cast<void*> (this))); } // ... }; |
Работает ! И у нас даже нет необходимости явно указывать шаблонные параметры для оператора приведения, поскольку компилятор выводит их автоматически из контекста использования :
Template<Base>& base = derived; |
К сожалению, используя этот способ, мы теряем контроль над происходящим, поскольку приведение будет успешным для любого аргумента шаблона U. Было бы замечательно добавить некие ограничения на эту операцию.. Но – опять же – в чем проблема ? Ведь мы можем добавить в код шаблонной функции-оператора кусочек кода, который будет выполнять преобразование указателя T* в указатель U*. И если это приведение корректно, то шаблонная функция успешно скомпилируется, в противном случае инстанцирования произведено не будет. Т.е. компилятор будет решать – насколько корректно приведение внешних шаблонов-классов по корректности приведения их шаблонных аргументов. Заодно можно сделать этот код никогда не выполняющимся, поскольку помимо верификации типов он никакой смысловой нагрузки не несет.
template <typename U> operator Template<U>& () { if (0) { T* derivedTypePtr = NULL; U* baseTypePtr = derivedTypePtr; (void) baseTypePtr; // А это может пригодиться для того, чтобы подавить // предупреждения компилятора о неиспользуемой переменной } return (*static_cast<Template<U>*> (static_cast<void*> (this))); } |
Таким образом – мы получаем безопасный по отношению к типам код, который полностью прозрачен для пользователя. Далее – вспоминаем про константность : следующий код не скомпилируется, даже если derived был изначально объявлен как const.
const Template<Base>& base = derived; |
Чтобы устранить это досадное недоразумение, достаточно написать константную версию оператора :
template <typename U> operator const Template<U>& () const { if (0) { T* derivedTypePtr = NULL; U* baseTypePtr = derivedTypePtr; (void) baseTypePtr; } return (*static_cast<const Template<U>*> (static_cast<const void*> (this))); } |
Все ! Осталось только разрешить пользователям все-таки применять небезопасное преобразование, если они точно уверены в том, что делают. Для этого напишем рядом специальную функцию, выполняющую такое же преобразование, но без дополнительных проверок (не забываем про const-версию) :
template <typename U> Template<U>& do_unsafe_cast() { return (*static_cast<Template<U>*> (static_cast<void*> (this))); } template <typename U> const Template<U>& do_unsafe_cast() const { return (*static_cast<const Template<U>*> (static_cast<const void*> (this))); } |
Заодно отметим, что в случае функции do_unsafe_cast() пользователю придется всегда явно указывать аргумент шаблона – и это очень хорошо, поскольку уточняет действия пользователя и к тому же дополнительно выделяется в коде :
Template<Derived>& derived = base.do_unsafe_cast<Derived>(); |
Такой вот получился аналог reinterpret_cast. К сожалению, для dynamic_cast метода преобразования создать не получилось (поскольку шаблонный класс Template в общем случае не может быть уверен в том, что тип, задаваемый аргументом шаблона, содержит виртуальные методы), но, думаю, здесь тоже можно найти какой-нибудь хитрый способ заставить компилятор сделать эффектный реверанс в сторону разработчика.
Мысли о применимости таких преобразований : скорее всего, это подойдет только для классов, которые хранят в себе лишь указатели на типы-аргументы (или ссылки), а не сами объекты целиком, поскольку в противном случае разные шаблоны будут инстанцироваться в совершенно различные по двоичной структуре классы, которые представляются в памяти по-разному, и интерпретация адреса одного шаблона в качестве указателя на другой скорее всего будет причиной непредсказуемого поведения. В моем случае подходящим шаблоном для такой модернизации стал шаблон умного указателя (в терминологии Джеффа Элджера – дескриптора) с возможностью подсчета ссылок.
PS. Не думаю, что описанный подход будет откровением для знатоков С++, которые используют шаблоны на полную катушку, но, надеюсь, что это будет кому-то полезно.
UPD: Внимание ! Этот код некорректно работает с компилятором GCC, поскольку GCC неверно
обрабатывает ситуации, связанные с преобразованием ссылок. В Visual C++ и Comeau все в норме.
Для того, чтобы эта особенность компилятора GCC не приводила к некорректной работе приложения,
придется избавиться от удобств перегруженных операторов преобразования и вместо них написать
аналогичные safe_cast-методы. Объяснение проблемы в следующем посте.
Недавно собрался и написал краткий ман по NBox’у на русском языке.
Что это такое и зачем нужно ?
NBox – утилита c открытым исходным кодом, предназначенная для сжатия множества дотнетовых сборок и файлов приложения в одну управляемую сборку, которая будет хранить сжатые сборки в себе и при необходимости загружать их динамически.
Для чего это может понадобиться ?
- Во-первых, для уменьшения размера дистрибутива (файлы сжимаются по алгоритму LZMA, используемому в популярном архиваторе 7-zip).
- Во-вторых, иногда для разработчика удобнее предоставлять дистрибутив одним исполняемым файлом
вместо того, чтобы делать полноценный инсталлятор или же распространять множество файлов в одном архиве (то есть этот инструмент можно использовать в качестве лайт-замены для инсталляторов). - В-третьих, загрузка приложения, в котором много зависимостей, занимает обычно больше времени, чем загрузка одного исполняемого файла с последующей подгрузкой необходимых модулей прямо в памяти (особенно это заметно на медленных сменных носителях), – и NBox можно использовать для оптимизации загрузки приложения.
Дополнительные особенности :
- Возможность включать в результирующую сборку не только managed-сборки, но и библиотеки с неуправляемым кодом. Неуправляемые библиотеки обычно используются через interop, и поэтому они должны быть извлечены перед запуском приложения. Обычно они извлекаются в ту же директорию, в которой расположен исполняемый файл, либо в системную директорию.
- Возможность включать любые файлы.
Да, вы можете засунуть любой файл и извлечь его перед запуском приложения в указанную директорию. Это может быть файл конфигурации приложения, какой-либо бинарник, звуковой файл или что-то совсем другое. - Корректная работа с WPF-приложениями.
Стандартные WPF-приложения особенным образом работает с ресурсами, поэтому обычный алгоритм для них не работает. Поэтому приходится слегка похимичить с ресурсами. Либо нужно дублировать ресурсы оригинальной сборки в сжатой, либо менять привязку к абсолютным путям на относительные в исходном коде и xaml.
Конец семестра приблизился как всегда незаметно. И, как водится, начались напряги со сдачей лабораторных работ по информатике. В семестре их всего 3 (надо сдать за 4 занятия), так что я с друзьями не особо сильно беспокоился о них и, в итоге, проспав первые 2 занятия и сдав одну на третьем, оказался в нехорошем положении, узнав о том, что преп свалил на конференцию, а 4ое занятие отменили. Оставалось ещё 1+1доп занятие с другой группой, на которых мне предстояло сдать 2 лабы. С нашим препом задача не из лёгких. Ну со второй я кое-как разобрался, а вот последняя представляла из себя программу, эмулирующую работу простейшей трёхадресной ЭВМ, поддерживающей команды ввода/вывода данных, арифметические операции и даже (!) условные переходы. По ходу выполнения работы студент должен пошагово выполнить несколько команд, указывая действия типа Чтения Счетчика Команд, Запись Адреса Команды на Шину Адреса и т.п. муть. Каждый тип команды (их 8) состоял из последовательности 8-15 операций, которые надо было по порядку запомнить. Шпора по этой лабе занимала лист тетрадного формата и легко палилась препом (из некоторых студенток он вытаскивал по 3 шпоры)) ), так что учить всё это желания не было, и я решил пойти другим путём. Результаты выполнения работы отображались на экране, надпись гласила о том, на каком месте программа завершила работу и о количестве допущенных студентом ошибок плюс общее кол-во ответов. Похожая табличка выводилась в лог проги. Зная, что прогу на компе в аудитории можно без проблем подменить, я составил адский план сдачи работы)).
0