Неявное приведение между родственными шаблонами С++
Сегодня столкнулся с интересной проблемой и нашел не менее интересный способ решения. Как всем известно, если мы имеем иерархию классов 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-методы. Объяснение проблемы в следующем посте.
0