Pull to refresh

dynamic_cast на этапе компиляции

Reading time10 min
Views3K

Приветствую все читающих.


О чём статья (или задача статьи): практический ответ на вопрос "возможно ли создать большой проект так, чтобы полностью отказаться от 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();
}

  1. Создание первого класса в иерархии 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.

  2. Создание второго класса в иерархии 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 — списке типов к которым может быть приведёт класс В, содержится класс А и все базовые типы класса А.


  3. Создание третьего класса в иерархии 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 содержится класс В, и все базовые типы класса В.


  4. Создание четвёртого класса в иерархии 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 содержит класс С и все его базовые типы.


  5. Проверка базовых типов классов
    Здесь и далее, структура 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 на этапе компиляции — это реально.
    Однако, вопрос о целесообразности остаётся насущным.


    Спасибо всем, кто прочитал статью :) — буду рад узнать ваш опыт, мнение по, или, возможно, даже решение описанной в статье задачи в комментариях.

Tags:
Hubs:
+8
Comments1

Articles