Привет, Хабр!

Сегодня мы рассмотрим библиотеку Loki, которая названа в честь одного из самых интересных и хитроумных богов скандинавской мифологии. Как и его мифологический тёзка, Loki в C++ отличается гибкостью и изяществом, имея мощные инструменты для метапрограммирования и реализации паттернов проектирования.

Принципы Loki

  1. Малое прекрасно:

    • Принцип заключается в минимизации внутренних зависимостей между компонентами библиотеки. Каждый компонент может быть использован независимо!

    • Например, существуют такие компоненты, как SmartPointer или TypeList, они могут быть использованы отдельно друг от друга.

  2. Мультипликативное превосходство:

    • Loki фокусируется на достижении сложных, спец. дизайнов через комбинацию простых и абстрактных компонентов.

  3. Минимальные зависимости:

    • Loki делает минимальные предположения о своем окружении и предоставляет гибкие точки расширения, чтобы можно было адаптировать библиотеку под нужды без необходимости вносить изменения в саму либу.

Основные компоненты Loki

Типы данных

TypeList — это метапрограммная структура данных, которая представляет собой список типов. Используют для манипуляций с типами на этапе компиляции.

Можно выполнять различные операции с TypeList: доступ к первому элементу, добавление и удаление элементов.

Например, доступ к первому элементу осуществляется с помощью front :

template <typename List>
struct Front;

template <typename Head, typename... Tail>
struct Front<TypeList<Head, Tail...>> {
    using Type = Head;
};

using FirstType = Front<MyTypes>::Type;  // int

Добавление элемента в начало списка с помощью PushFront:

template <typename List, typename NewType>
struct PushFront;

template <typename... Types, typename NewType>
struct PushFront<TypeList<Types...>, NewType> {
    using Type = TypeList<NewType, Types...>;
};

using NewList = PushFront<MyTypes, char>::Type;  // TypeList<char, int, double, std::string>
template <typename List, typename NewType>
struct PushFront;

template <typename... Types, typename NewType>
struct PushFront<TypeList<Types...>, NewType> {
    using Type = TypeList<NewType, Types...>;
};

using NewList = PushFront<MyTypes, char>::Type;  // TypeList<char, int, double, std::string>

Еще существует NullType, который используется для обозначения конца списка типов или отсутствия типа.

struct NullType {};

using EmptyList = TypeList<>;

Умные указатели

Умные указатели в Loki представляют из себя шаблоны для управления ресурсами и автоматического освобождения памяти:

#include <loki/SmartPtr.h>

using namespace Loki;

typedef SmartPtr<int> IntPtr;

void example() {
    IntPtr p(new int(10));
    // автоматическое освобождение памяти при выходе из области видимости p
}

Мультиметоды

Мультиметоды позволяют реализовать множественную диспетчеризацию в C++. Так можно вызывать различные функции в зависимости от типов аргументов во время выполнения:

#include <loki/MultiMethods.h>

using namespace Loki;

struct Base {
    virtual ~Base() {}
};

struct Derived1 : Base {};
struct Derived2 : Base {};

void function(Derived1&) { /* ... */ }
void function(Derived2&) { /* ... */ }

int main() {
    Derived1 d1;
    Derived2 d2;
    Base* b1 = &d1;
    Base* b2 = &d2;

    // вызов функции в зависимости от типа аргумента
    Invoke(function, *b1);
    Invoke(function, *b2);
}

Функторы

Функторы предоставляют объект-ориентированный способ работы с функциями:

#include <loki/Functor.h>

using namespace Loki;

void print(int x) {
    std::cout << x << std::endl;
}

int main() {
    Functor<void, LOKI_TYPELIST_1(int)> f(print);
    f(10);  // Выведет 10
}

Паттерны проектирования: одиночка, фабрика, посетитель

Есть реализация паттерна одиночка с возможностью управления временем жизни объекта:

#include <loki/Singleton.h>

using namespace Loki;

class MySingleton {
public:
    void DoSomething() { /* ... */ }
};

int main() {
    MySingleton& instance = Singleton<MySingleton>::Instance();
    instance.DoSomething();
}

Фабрика позволяет создавать объекты различных классов, не зная их точных типов:

#include <loki/Factory.h>

using namespace Loki;

class Base {
public:
    virtual ~Base() {}
    virtual void DoSomething() = 0;
};

class Derived : public Base {
public:
    void DoSomething() override { std::cout << "Derived" << std::endl; }
};

int main() {
    typedef Factory<AbstractFactoryUnit, Base, int> MyFactory;
    MyFactory factory;
    factory.Register<Derived>(1);
    std::unique_ptr<Base> obj(factory.CreateObject(1));
    obj->DoSomething();  // Выведет "Derived"
}

Посетитель позволяет добавлять новые операции к существующим объектам без изменения их классов:

#include <loki/Visitor.h>

using namespace Loki;

class Element {
public:
    virtual void Accept(Visitor<void, Element>& visitor) = 0;
};

class ConcreteElement : public Element {
public:
    void Accept(Visitor<void, Element>& visitor) override {
        visitor.Visit(*this);
    }
};

class ConcreteVisitor : public Visitor<void, Element> {
public:
    void Visit(ConcreteElement& element) {
        std::cout << "Visited ConcreteElement" << std::endl;
    }
};

int main() {
    ConcreteElement elem;
    ConcreteVisitor visitor;
    elem.Accept(visitor);  // выведет "Visited ConcreteElement"
}

Политика-ориентированный дизайн

Политика-ориентированный дизайн позволяет создавать классы, которые могут быть настроены с помощью шаблонных параметров. Достигается за счет разбиения поведения класса на несколько политик, каждая из которых реализует определенную функциональность:

template <typename T, typename ThreadingModel>
class Singleton {
public:
    static T& Instance() {
        static T instance;
        return instance;
    }
private:
    Singleton() {}
    ~Singleton() {}
};

class SingleThreaded {
public:
    typedef int Lock;
};

class MultiThreaded {
public:
    class Lock {
    public:
        Lock() { /* lock mechanism */ }
        ~Lock() { /* unlock mechanism */ }
    };
};

typedef Singleton<MyClass, SingleThreaded> MySingleton;

Примеры использования

TypeList для динамического создания объектов:

#include <iostream>
#include <loki/Typelist.h>
#include <loki/Factory.h>

using namespace Loki;

// определяем несколько классов
class A { public: void Display() { std::cout << "Class A\n"; } };
class B { public: void Display() { std::cout << "Class B\n"; } };
class C { public: void Display() { std::cout << "Class C\n"; } };

// создаем TypeList из классов
typedef TYPELIST_3(A, B, C) MyTypeList;

// фабрика для создания объектов из TypeList
typedef Factory<AbstractFactoryUnit, MyTypeList> MyFactory;

int main() {
    MyFactory factory;
    factory.Register<A>(1);
    factory.Register<B>(2);
    factory.Register<C>(3);

    std::unique_ptr<A> a(factory.CreateObject(1));
    std::unique_ptr<B> b(factory.CreateObject(2));
    std::unique_ptr<C> c(factory.CreateObject(3));

    a->Display();
    b->Display();
    c->Display();

    return 0;
}

Так можно динамически создавать объекты различных типов, используя TypeList и фабрику.

Управление памятью с помощью SmartPtr:

#include <iostream>
#include <loki/SmartPtr.h>

using namespace Loki;

class Resource {
public:
    Resource() { std::cout << "Resource Acquired\n"; }
    ~Resource() { std::cout << "Resource Released\n"; }
};

void useResource() {
    SmartPtr<Resource> res(new Resource());
    // ресурс автоматом освободится в конце блока
}

int main() {
    useResource();
    return 0;
}

Реализация многометодного диспетчера:

#include <iostream>
#include <loki/MultiMethods.h>

using namespace Loki;

class Animal {
public:
    virtual ~Animal() {}
    LOKI_DEFINE_VISITABLE()
};

class Dog : public Animal {
public:
    void Bark() { std::cout << "Woof!\n"; }
    LOKI_DEFINE_VISITABLE()
};

class Cat : public Animal {
public:
    void Meow() { std::cout << "Meow!\n"; }
    LOKI_DEFINE_VISITABLE()
};

void Speak(Animal&) { std::cout << "Unknown animal sound\n"; }
void Speak(Dog& d) { d.Bark(); }
void Speak(Cat& c) { c.Meow(); }

int main() {
    Dog dog;
    Cat cat;
    Animal* animals[] = { &dog, &cat };

    for (Animal* animal : animals) {
        Invoke(Speak, *animal);
    }

    return 0;
}

Functor для обратных вызовов

#include <iostream>
#include <loki/Functor.h>

using namespace Loki;

void PrintNumber(int number) {
    std::cout << "Number: " << number << std::endl;
}

int main() {
    Functor<void, LOKI_TYPELIST_1(int)> func(PrintNumber);
    func(42);
    return 0;
}

Подробнее с библиотекой можно ознакомиться здесь.

В завершение хочу рассказать об открытых уроках для разработчиков на C++, которые совсем скоро пройдут в Otus:

  • 11 июня: Условные переменные в С++. Узнаете, что такое std::condition_variable, какие задачи он решает и типовые ошибки при его использовании. Узнаете, что такое spurious wakeup и напишите несколько concurrency-примитивов на основе condition_variable. Записаться бесплатно можно по ссылке.

  • 24 июня: Как разработчику на С++ организовать кроссплатформенную разработку? На этом уроке узнаете, как решить проблему поиска зависимостей, напишите conan-файл и сможете организовать свой сервер пакетов в своей экосистеме CI/CD. Запись по ссылке.

Комментарии (11)


  1. Keeper10
    06.06.2024 14:33
    +3

    Александреску, перелогиньтесь.


  1. wataru
    06.06.2024 14:33
    +3

    Поначалу возник вопрос, а зачем это все нужно. Чем, например, предложенный SmartPtr лучше std::unique_ptr или std::shared_ptr. Ответ в исходниках на гитхабе весьма четок:

    Last update: Aug 29, 2002

    Часть ссылок ведет на давно уже разделегированные домены, а часть на sourceforge, где есть апдейты аж 2009 года, правда там похоже только мелкие правки последние несколько лет были.

    Текст статьи непонятно, откуда взялся, ибо приблуды для мультиметодов Invoke() в исходниках вообще, например, нет.


    1. unreal_undead2
      06.06.2024 14:33

      А в разделе про typelist вариадики из C++11 - с ними то и дурак напишет, но Александреску это делал ещё на оригинальном стандарте 98го года.


  1. MountainGoat
    06.06.2024 14:33

    Я знаю, что это очень древняя библиотека, но всё равно не могу избавиться от мысли, что это всё задумывалось как отчаянная попытка не переходить на Rust. :) Всё что в ней было годного давно или в std или в boost. А когда шаблон на шаблоне едет, шаблоном погоняет - потом отлаживать это то ещё удовольствие. Дебаггер в таких проектах просто показывает на рандомную строчку.


    1. GlukKazan
      06.06.2024 14:33
      +1

      Не было тогда ещё Раста. И D тоже пока ещё не было.

      Выживали как могли...


  1. ImagineTables
    06.06.2024 14:33

    Принцип заключается в минимизации внутренних зависимостей между компонентами библиотеки. Каждый компонент может быть использован независимо!

    Как же я не люблю спроектированные таким образом библиотеки. Если я тащу библиотеку в проект, у меня так или иначе появляется зависимость. Уплатив эту цену, я хочу все удобства связности. Это, конечно, относится именно к библиотекам, а не набору разрозненных компонентов, зачем-то собранных в одно целое.


    1. zorn-v100500
      06.06.2024 14:33

      Возьмем к примеру symfony (это их php)

      Зачем мне тащить в проект компонент для обработки очередей, если мне надо отправлять почту ?

      а не набору разрозненных компонентов, зачем-то собранных в одно целое.

      Ну видимо для того чтобы потом их разделить ?


  1. Daemonis
    06.06.2024 14:33

    // фабрика для создания объектов из TypeList
    typedef Factory<AbstractFactoryUnit, MyTypeList> MyFactory;
    
    int main() {
        MyFactory factory;
        factory.Register<A>(1);
        factory.Register<B>(2);
        factory.Register<C>(3);

    А зачем передавать список типов в шаблон, если потом каждый из них все равно руками регистрировать?


  1. Readme
    06.06.2024 14:33
    +5

    Отвратительно, OTUS. Копирайтер откопал древнюю либу с кликбейтным названием, и попросил ChatGPT её кратко описать (отсюда и нестыковки по функциональности).

    Тем не менее, сама идея и реализация Loki господином Александреску в своё время были весьма интересны, и во многом предвосхитили фичи будущего стандарта C++11. Если бы статья была обзором-ретроспективой с высоты текущих C++23/26, было бы круто. Но, видимо, перехочется — не в этой рекламной брошюрке.


    1. wataru
      06.06.2024 14:33

      и попросил ChatGPT её кратко описать (отсюда и нестыковки по функциональности).

      Хм, а это весьма правдоподобное объяснение! Ясно, откуда тот же Invoke вылез, ибо ни гугл, ни бинг с яндексом нигде loki invoke ненаходят. Куски кода из примеров нигде не встречаются.


  1. Playa
    06.06.2024 14:33

    Популяризация некрофилии.