Опишу одну из непопулярных сторон C++. Язык поддерживает множественное наследование и, соответственно, в нем существует т.н. проблема ромба. Стандарт содержит несколько решений этой проблемы: виртуальное наследование, виртуальные деструкторы и явное указание на базовый класс (в случае с шаблонами). О последнем и пойдет речь.
Считается, что С++ не поддерживает повторное наследование т.к. нельзя явно указать на используемый базовый класс (википедия это мнение разделяет). Буквальное повторное наследование в стиле:
Конечно, недопустимо и противоречит здравому смыслу. Косвенное повторное наследование в стиле:
Доступно (с предупреждением компилятора), но так же лишено смысла.
Но не для случая, когда базовый класс шаблонный. Самое интересное начинается, когда базовый класс содержит шаблонную логику, например, применимую к классам-потомкам. Так, ситуация, когда надо получить свой определяемый тип «умный указатель» Ptr для каждого класса-потомка, запутает компилятор:
Компилятор увидит явную неоднозначность — какой из трёх типов A::Ptr, B::Ptr или C::Ptr выбрать, и выдаст ошибку. Эта проблема многих вводит в ступор, часто источники предлагают переименовать части базовых классов и исказить базовую архитектуру. Тем не менее, стандарт поддерживает разрешение неоднозначности — явное указание нужного базового класса через директиву using. Например:
Так, если явно указать, какую ветвь наследования применить внутри конкретного класса-потомка:
То неоднозначность исчезнет, компилятор код скомпилирует, а в копилке будет еще одно универсальное решение.
Считается, что С++ не поддерживает повторное наследование т.к. нельзя явно указать на используемый базовый класс (википедия это мнение разделяет). Буквальное повторное наследование в стиле:
struct A
{
};
struct B : A, A
{
};
Конечно, недопустимо и противоречит здравому смыслу. Косвенное повторное наследование в стиле:
struct A
{
};
struct B : A
{
};
struct C : B, A
{
};
Доступно (с предупреждением компилятора), но так же лишено смысла.
Но не для случая, когда базовый класс шаблонный. Самое интересное начинается, когда базовый класс содержит шаблонную логику, например, применимую к классам-потомкам. Так, ситуация, когда надо получить свой определяемый тип «умный указатель» Ptr для каждого класса-потомка, запутает компилятор:
#include <memory>
#include <iostream>
#include <typeinfo.h>
template<typename T>
struct CSmartPointer
{
typedef std::tr1::shared_ptr<T> Ptr;
};
struct A : CSmartPointer<A>
{
};
struct B : A, CSmartPointer<B>
{
};
struct C : B, CSmartPointer<C>
{
};
void main()
{
std::cout << typeid(C::Ptr).name() << std::endl;
}
Компилятор увидит явную неоднозначность — какой из трёх типов A::Ptr, B::Ptr или C::Ptr выбрать, и выдаст ошибку. Эта проблема многих вводит в ступор, часто источники предлагают переименовать части базовых классов и исказить базовую архитектуру. Тем не менее, стандарт поддерживает разрешение неоднозначности — явное указание нужного базового класса через директиву using. Например:
using CSmartPointer<B>::Ptr;
Так, если явно указать, какую ветвь наследования применить внутри конкретного класса-потомка:
#include <memory>
#include <iostream>
#include <typeinfo.h>
template<typename T>
struct CSmartPointer
{
typedef std::tr1::shared_ptr<T> Ptr;
};
struct A : CSmartPointer<A>
{
};
struct B : A, CSmartPointer<B>
{
using CSmartPointer<B>::Ptr;
};
struct C : B, CSmartPointer<C>
{
using CSmartPointer<C>::Ptr;
};
void main()
{
std::cout << typeid(A::Ptr).name() << std::endl;
std::cout << typeid(B::Ptr).name() << std::endl;
std::cout << typeid(C::Ptr).name() << std::endl;
}
То неоднозначность исчезнет, компилятор код скомпилирует, а в копилке будет еще одно универсальное решение.