Приветствую все читающих.
О чём статья (или задача статьи): практический ответ на вопрос "возможно ли создать большой проект так, чтобы полностью отказаться от dynamic_cast
на этапе выполнения?", где под большим проектом подразумевает такой в котором уже нет человека, что бы держал в голове всю кодовую базу проекта целиком.
Предварительный ответ: ДА, это возможно — возможно создать механизм, позволяющий решить задачу dynamic_cast на этапе компиляции, но — едва ли подобное будет применяться на практике по причинам как: (1) с самого начала целевой проект должен строиться согласно наперёд заданным правилам, в следствии чего взять и применить методику к существующему проекту, очень трудоёмко (2) существенное повышение сложности кода с точки зрения удобства его читаемости в определённых местах, где, собственно, происходит замена логики dynamic_cast
на предложенную ниже (3) использование шаблонов, что может быть неприемлемым в некоторых проектах по идеологическим соображениям ответственных за него (4) интерес автора исключительно в том, чтобы дать ответ на поставленный вопрос, а не в том, чтобы создать универсальный и удобный механизм решения поставленной задачи (в конце-концов, не нужно на практике решать проблемы, которые не являются насущными).
Идея реализации
За основу была взята идея списка типов, описанная Андреем Александреску и реализованная им же в библиотеке Loki. Данная идея была доработана по следующим пунктам (пункты помеченные *
означают, что по данному пункту автор статьи не согласен с видением Александреску по поводу списков типов):
- добавлена возможность генерации произвольного по длине списка типов без использования макросов и/или шаблонных структур, с количеством шаблонных параметров равных длине создаваемого списка;
- добавлена возможность генерации списка типов на основе типа(ов) и/или существующего списка(ов) типов в их произвольной комбинации;
- *удалена возможность создавать списки типов элементы которых могут являться списками типов;
- *удалены функции
MostDerived
иDerivedToFront
как и любая логика завязанная на наследовании классов, т.к. (1) логика её работы сильно зависит от порядка типов в списке типов, а потому требует бдительности от программиста при создании соответствующих списков и, что более важно, полного знания проекта программистом, который будет применять эту логику, что противоречит условиям задачи (2) распознавание наследования вниз по иерархии наследования, вообще говоря, простая задача не требующая какой-либо дополнительной логики этапа компиляции помимо уже имеющейся, тогда как автора статьи интересует в первую очередь логика распознавания наследования вверх на этапе компиляции, в чём выше обозначенные функции помочь не в силах; - добавлены проверки через
static_assert
, что позволяет получать осмысленные сообщения об ошибках при компиляции списка типов, в случае возникновения таковых; - добавлены функции как
RemoveFromSize
,CutFromSize
позволяющие получать подсписки списков типов.
Итоговый код библиотеки, для работы со списками типов, в полном виде вы можете посмотреть здесь (https://github.com/AlexeyPerestoronin/Cpp_TypesList), тогда как в статье будет присутствовать код, непосредственно использующий функционал данной библиотеки, необходимый для решения задачи.
Для начала будет представлен весь тестовый код с демонстрацией решения поставленной задачи, а далее, один за одним будут представлены и объяснены фрагменты из этого кода, последовательное ознакомление с которыми должно прояснить задумку автора.
#include <gtest/gtest.h>
#include <TypesList.hpp>
#include <memory>
class A {
public:
using BASE_t = TL::Refine_R<TL::CreateTypesList_R<void>>;
A() {}
A(int a) {
buffer << ' ' << a;
}
virtual void F1() = 0;
protected:
std::stringstream buffer;
};
class B : public A {
public:
using BASE_t = TL::Refine_R<TL::CreateTypesList_R<A, A::BASE_t>>;
B() {}
B(int a, int b)
: A(a) {
buffer << ' ' << b;
}
virtual void F1() override {
std::cout << "class::B" << buffer.str() << std::endl;
}
};
class C : public B {
public:
using BASE_t = TL::Refine_R<TL::CreateTypesList_R<B, B::BASE_t>>;
C() {}
C(int a, int b, int c)
: B(a, b) {
buffer << ' ' << c;
}
virtual void F1() override {
std::cout << "class::C" << buffer.str() << std::endl;
}
};
class D : public C {
public:
using BASE_t = TL::Refine_R<TL::CreateTypesList_R<C, C::BASE_t>>;
D() {}
D(int a, int b, int c, int d)
: C(a, b, c) {
buffer << ' ' << d;
}
virtual void F1() override {
std::cout << "class::D" << buffer.str() << std::endl;
}
};
TEST(Check_class_bases, test) {
{
using TClass = A;
EXPECT_EQ(TClass::BASE_t::size, 1);
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));
}
{
using TClass = B;
EXPECT_EQ(TClass::BASE_t::size, 2);
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));
}
{
using TClass = C;
EXPECT_EQ(TClass::BASE_t::size, 3);
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>));
}
{
using TClass = D;
EXPECT_EQ(TClass::BASE_t::size, 4);
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>));
EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, C>));
}
}
// TT - Type to Type
template<class Type, class BASE_t>
struct T2T {
std::shared_ptr<Type> value;
using PossibleTo_t = BASE_t;
};
template<class To, class From, class... Arguments>
auto T2TMake(Arguments&&... arguments) {
T2T<To, TL::Refine_R<TL::CreateTypesList_R<From, From::BASE_t>>> result{};
result.value = std::make_shared<From>(arguments...);
return result;
}
template<class BASE_t>
void AttemptUse(T2T<A, BASE_t> tb) {
static_assert(TL::IsInList_R<BASE_t, C>, "this function can to use only with C-derivative params");
tb.value->F1();
}
TEST(T2TMake, test) {
{
auto value = T2TMake<A, B>();
using TClass = decltype(value)::PossibleTo_t;
EXPECT_EQ(TClass::size, 3);
EXPECT_TRUE((TL::IsInList_R<TClass, void>));
EXPECT_TRUE((TL::IsInList_R<TClass, A>));
EXPECT_TRUE((TL::IsInList_R<TClass, B>));
// AttemptUse(value); // compilation error
}
{
auto value = T2TMake<A, B>(1, 2);
using TClass = decltype(value)::PossibleTo_t;
EXPECT_EQ(TClass::size, 3);
EXPECT_TRUE((TL::IsInList_R<TClass, void>));
EXPECT_TRUE((TL::IsInList_R<TClass, A>));
EXPECT_TRUE((TL::IsInList_R<TClass, B>));
// AttemptUse(value); // compilation error
}
{
auto value = T2TMake<A, C>();
using TClass = decltype(value)::PossibleTo_t;
EXPECT_EQ(TClass::size, 4);
EXPECT_TRUE((TL::IsInList_R<TClass, void>));
EXPECT_TRUE((TL::IsInList_R<TClass, A>));
EXPECT_TRUE((TL::IsInList_R<TClass, B>));
EXPECT_TRUE((TL::IsInList_R<TClass, C>));
AttemptUse(value);
}
{
auto value = T2TMake<A, C>(1, 2, 3);
using TClass = decltype(value)::PossibleTo_t;
EXPECT_EQ(TClass::size, 4);
EXPECT_TRUE((TL::IsInList_R<TClass, void>));
EXPECT_TRUE((TL::IsInList_R<TClass, A>));
EXPECT_TRUE((TL::IsInList_R<TClass, B>));
EXPECT_TRUE((TL::IsInList_R<TClass, C>));
AttemptUse(value);
}
{
auto value = T2TMake<A, D>();
using TClass = decltype(value)::PossibleTo_t;
EXPECT_EQ(TClass::size, 5);
EXPECT_TRUE((TL::IsInList_R<TClass, void>));
EXPECT_TRUE((TL::IsInList_R<TClass, A>));
EXPECT_TRUE((TL::IsInList_R<TClass, B>));
EXPECT_TRUE((TL::IsInList_R<TClass, C>));
EXPECT_TRUE((TL::IsInList_R<TClass, D>));
AttemptUse(value);
}
{
auto value = T2TMake<A, D>(1, 2, 3, 4);
using TClass = decltype(value)::PossibleTo_t;
EXPECT_EQ(TClass::size, 5);
EXPECT_TRUE((TL::IsInList_R<TClass, void>));
EXPECT_TRUE((TL::IsInList_R<TClass, A>));
EXPECT_TRUE((TL::IsInList_R<TClass, B>));
EXPECT_TRUE((TL::IsInList_R<TClass, C>));
EXPECT_TRUE((TL::IsInList_R<TClass, D>));
AttemptUse(value);
}
}
int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Создание первого класса в иерархии
class A
class A { public: using BASE_t = TL::Refine_R<TL::CreateTypesList_R<void>>; A() {} A(int a) { buffer << ' ' << a; } virtual void F1() = 0; protected: std::stringstream buffer; };
class A
— это простой чисто-вирутуальный класс, главной особенностью которого является строка:using BASE_t = TL::Refine_R<TL::CreateTypesList_R<void>>;
, которая определяет для класса список типов от которых наследуется класс.
Здесь и далее:
TL::CreateTypesList_R
— структура, создающая список типов произвольной длинны.TL::Refine_R
— структура, очищающая список типов от дубликатов, в случае наличия таковых.
Т.к. класс А ни от кого не наследуется, то единственным типом к которому он может быть напрямую приведён являетсяvoid
.
Создание второго класса в иерархии
class B
class B : public A { public: using BASE_t = TL::Refine_R<TL::CreateTypesList_R<A, A::BASE_t>>; B() {} B(int a, int b) : A(a) { buffer << ' ' << b; } virtual void F1() override { std::cout << "class::B" << buffer.str() << std::endl; } };
Как видим, класс В наследуется от класса А, а потому в его
BASE_t
— списке типов к которым может быть приведёт класс В, содержится класс А и все базовые типы класса А.
Создание третьего класса в иерархии
class C
class C : public B { public: using BASE_t = TL::Refine_R<TL::CreateTypesList_R<B, B::BASE_t>>; C() {} C(int a, int b, int c) : B(a, b) { buffer << ' ' << c; } virtual void F1() override { std::cout << "class::C" << buffer.str() << std::endl; } };
Класс С, является наследником класса В, следовательно в его
BASE_t
содержится класс В, и все базовые типы класса В.
Создание четвёртого класса в иерархии
class D
class D : public C { public: using BASE_t = TL::Refine_R<TL::CreateTypesList_R<C, C::BASE_t>>; D() {} D(int a, int b, int c, int d) : C(a, b, c) { buffer << ' ' << d; } virtual void F1() override { std::cout << "class::D" << buffer.str() << std::endl; } };
Аналогично предыдущему классу, класс D наследуется от класса С и его
BASE_t
содержит класс С и все его базовые типы.
- Проверка базовых типов классов
Здесь и далее, структураTL::IsInList_R<TypesList, Type>
возвращаетtrue
когда и только тогда, когда типType
входит в список типовTypesList
, иfalse
— в противном случае.
TEST(Check_class_bases, test) { { using TClass = A; EXPECT_EQ(TClass::BASE_t::size, 1); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>)); } { using TClass = B; EXPECT_EQ(TClass::BASE_t::size, 2); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>)); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>)); } { using TClass = C; EXPECT_EQ(TClass::BASE_t::size, 3); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>)); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>)); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>)); } { using TClass = D; EXPECT_EQ(TClass::BASE_t::size, 4); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>)); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>)); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>)); EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, C>)); } }
Как видно из фрагмента кода, каждый из созданных классов:
class A
,class B
,class C
иclass D
, — содержит в своёмBASE_t
типы к которым этот класс может быть приведён вниз по иерархии наследования.
Создание структуры с информацией о наследовании вверх по иерархии
// T2T - Type to Type template<class Type, class BASE_t> struct T2T { std::shared_ptr<Type> value; using PossibleTo_t = BASE_t; };
Данная структура предназначена для хранения указателя на экземпляр
value
типаType
и списка типовPossibleTo_t
к которымvalue
может быть приведён, включая типы выше по иерархии от (унаследованные от)Type
.
Создание функции для создания структурыT2T
template<class To, class From, class... Arguments> auto T2TMake(Arguments&&... arguments) { T2T<To, TL::Refine_R<TL::CreateTypesList_R<From, From::BASE_t>>> result{}; result.value = std::make_shared<From>(arguments...); return result; }
Шаблонные параметры функции
T2TMake
имеют следующее предназначение:
From
— истинный тип объекта, создаваемого для хранения в структуреT2T
;To
— тип под которым созданный объект хранится в структуреT2T
;Arguments
— типы аргументов для создания целевого объекта.
Как видно, данная фукнция будет компилироваться только в случае, если типFrom
является наследником типаTo
, а записьTL::Refine_R<TL::CreateTypesList_R<From, From::BASE_t>>
создаёт список типов для структурыT2T
по которому в дальнейшем можно будeт однозначно определить всё множество типов к которым может быть приведён указатель на объектvalue
.
Создание функции для проверки корректности работы структурыT2T
template<class BASE_t> void AttemptUse(T2T<A, BASE_t> tb) { static_assert(TL::IsInList_R<BASE_t, C>, "this function can to use only with C-derivative params"); tb.value->F1(); }
По придуманным условиям, данная функция может работать с объектами класса А, являющимися приведёнными от объектов не ниже класса С по иерархии типов, причём, — и это самое важное, — данное условие проверяется ещё на этапе компиляции.
Итоговое тестирование созданной логики
TEST(T2TMake, test) { { auto value = T2TMake<A, B>(); using TClass = decltype(value)::PossibleTo_t; EXPECT_EQ(TClass::size, 3); EXPECT_TRUE((TL::IsInList_R<TClass, void>)); EXPECT_TRUE((TL::IsInList_R<TClass, A>)); EXPECT_TRUE((TL::IsInList_R<TClass, B>)); // AttemptUse(value); // compilation error } { auto value = T2TMake<A, B>(1, 2); using TClass = decltype(value)::PossibleTo_t; EXPECT_EQ(TClass::size, 3); EXPECT_TRUE((TL::IsInList_R<TClass, void>)); EXPECT_TRUE((TL::IsInList_R<TClass, A>)); EXPECT_TRUE((TL::IsInList_R<TClass, B>)); // AttemptUse(value); // compilation error } { auto value = T2TMake<A, C>(); using TClass = decltype(value)::PossibleTo_t; EXPECT_EQ(TClass::size, 4); EXPECT_TRUE((TL::IsInList_R<TClass, void>)); EXPECT_TRUE((TL::IsInList_R<TClass, A>)); EXPECT_TRUE((TL::IsInList_R<TClass, B>)); EXPECT_TRUE((TL::IsInList_R<TClass, C>)); AttemptUse(value); } { auto value = T2TMake<A, C>(1, 2, 3); using TClass = decltype(value)::PossibleTo_t; EXPECT_EQ(TClass::size, 4); EXPECT_TRUE((TL::IsInList_R<TClass, void>)); EXPECT_TRUE((TL::IsInList_R<TClass, A>)); EXPECT_TRUE((TL::IsInList_R<TClass, B>)); EXPECT_TRUE((TL::IsInList_R<TClass, C>)); AttemptUse(value); } { auto value = T2TMake<A, D>(); using TClass = decltype(value)::PossibleTo_t; EXPECT_EQ(TClass::size, 5); EXPECT_TRUE((TL::IsInList_R<TClass, void>)); EXPECT_TRUE((TL::IsInList_R<TClass, A>)); EXPECT_TRUE((TL::IsInList_R<TClass, B>)); EXPECT_TRUE((TL::IsInList_R<TClass, C>)); EXPECT_TRUE((TL::IsInList_R<TClass, D>)); AttemptUse(value); } { auto value = T2TMake<A, D>(1, 2, 3, 4); using TClass = decltype(value)::PossibleTo_t; EXPECT_EQ(TClass::size, 5); EXPECT_TRUE((TL::IsInList_R<TClass, void>)); EXPECT_TRUE((TL::IsInList_R<TClass, A>)); EXPECT_TRUE((TL::IsInList_R<TClass, B>)); EXPECT_TRUE((TL::IsInList_R<TClass, C>)); EXPECT_TRUE((TL::IsInList_R<TClass, D>)); AttemptUse(value); } }
Выводы
dynamic_cast
на этапе компиляции — это реально.
Однако, вопрос о целесообразности остаётся насущным.
Спасибо всем, кто прочитал статью :) — буду рад узнать ваш опыт, мнение по, или, возможно, даже решение описанной в статье задачи в комментариях.