Некогда мне пришлось столкнуться со следующей проблемой: требуется, чтобы в качестве шаблонного параметра классу могли быть переданы лишь наследники определенного класса.
Конечно, при инстанцировании шаблона будет произведена проверка наличия вызываемых у шаблонного параметра методов. Но этого бывает не достаточно, если:
Принцип работы достаточно прост. Деструктор инстанцируется всегда, поэтому код в нем всегда компилируется. Однако у этого метода есть следующие недостатки:
Конечно, при инстанцировании шаблона будет произведена проверка наличия вызываемых у шаблонного параметра методов. Но этого бывает не достаточно, если:
- необходимо, чтобы шаблонный параметр не просто удовлетворял концепции, а реализовывал определенный интерфейс;
- пока метод, использующий функции-члены этого интерфейса не будет инстанцирован, мы не получим ошибки компиляции.
template<typename T>
class TSomeClass {
public:
~TSomeClass() {
T *p = 0;
TBase *pb = static_cast<TBase*>(p);
pb; // для подавления предупреждения компилятора о неиспользуемой переменной
}
};- в run-time в деструкторе выполняется лишний код, пусть и совсем незначительный;
- метод работает в одну сторону, т.е. мы не можем использовать его для условной компиляции; код либо скомпилирован, если условие наследования выполнилось, либо нет.
Теперь рассмотрим намного более интересное и мощное решение этой проблемы. Его я встретил в книге Герба Саттера "Решение сложных задач на С++" (Задача 1.11 "Расширяемые шаблоны").
Требование наличия функций-членов
В приведенном ниже коде осуществляется проверка наличия у шаблонного типа функции-члена с определенной сигнатурой.
template<typename T>
class HasClone {
public:
static void Constraints() {
T* (T::*test)() const = &T::Clone;
test; // для подавления предупреждения компилятора о неиспользуемой переменной
}
HasClone() { void (*p)() = Constraints; }
};
Метод Contstaints() здесь введен для обобщения: все подобные проверки помещаем в него. Сама идея проверки заключается в присваивании указателей на функции. Если сигнатуры функций не совпадают (или функция с таким именем отсутствует) код не скомпилируется. Использовать приведенный класс достаточно просто:
class TFoo : HasClone<TFoo> {
...
};
Как видите, можно использовать приватное наследование, т.к. по сути мы не используем никакой функциональности этого класса в run-time. Это решение обладает теми же недостатками, что и мое, но оно обобщенное и может быть легко использовано.
Проверка наследования
Для проверки наследования шаблонного параметра от определенного класса Саттер приводит следующие 2 способа.
// Способ 1
template<typename D, typename B>
class IsDerivedFrom1 {
private:
class No {};
class Yes { No no[2]; };
static Yes Test(B*); // Объявлена, но не определена
static No Test(...); // Объявлена, но не определена
public:
enum
{ Is = sizeof(Test(static_cast<D*>(0))) == sizeof(Yes) };
};
// Способ 2
template<typename D, typename B>
class IsDerivedFrom2 {
private:
static void Constraints(D* p) {
B *pb = p;
pb = p;
}
protected:
IsDerivedFrom2() { void(*p)(D*) = Constraints; }
};
// Специализация для типа void, т.к. любой указатель преобразуется в void* и обратно
template<typename D>
class IsDerivedFrom2 {
IsDerivedFrom2 { char *p = (int*)0; /* Error */ }
};
Первый способ не так прост для понимания, зато он дает возможность не просто "ругнуться" во время компиляции, а получить выражение, которое можно использовать. Например, нам необходимо написать специализацию шаблона для всех типов, являющихся наследниками некоторого базового. Обычно в таком случае это решается копи-пастом специализаций для всей иерархии. Думаю, нет смысла объяснять, что такое решение негибкое, нерасширяемое и чревато ошибками. Зато вот как эту проблему можно решить, используя класс IsDerivedFrom1:
template<typename T, int>
class XImpl {
...
};
template<typename T>
class XImpl<T, 1> {
...
};
template<typename T>
class X {
XImpl<T, IsDerivedFrom1<T, Cloneable>::Is> impl_;
};
Комментариев нет:
Отправить комментарий