Привет, Хабр!
Сегодня мы рассмотрим библиотеку Loki, которая названа в честь одного из самых интересных и хитроумных богов скандинавской мифологии. Как и его мифологический тёзка, Loki в C++ отличается гибкостью и изяществом, имея мощные инструменты для метапрограммирования и реализации паттернов проектирования.
Принципы Loki
-
Малое прекрасно:
Принцип заключается в минимизации внутренних зависимостей между компонентами библиотеки. Каждый компонент может быть использован независимо!
Например, существуют такие компоненты, как SmartPointer или TypeList, они могут быть использованы отдельно друг от друга.
-
Мультипликативное превосходство:
Loki фокусируется на достижении сложных, спец. дизайнов через комбинацию простых и абстрактных компонентов.
-
Минимальные зависимости:
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)
wataru
06.06.2024 14:33+3Поначалу возник вопрос, а зачем это все нужно. Чем, например, предложенный SmartPtr лучше std::unique_ptr или std::shared_ptr. Ответ в исходниках на гитхабе весьма четок:
Last update: Aug 29, 2002
Часть ссылок ведет на давно уже разделегированные домены, а часть на sourceforge, где есть апдейты аж 2009 года, правда там похоже только мелкие правки последние несколько лет были.
Текст статьи непонятно, откуда взялся, ибо приблуды для мультиметодов Invoke() в исходниках вообще, например, нет.
unreal_undead2
06.06.2024 14:33А в разделе про typelist вариадики из C++11 - с ними то и дурак напишет, но Александреску это делал ещё на оригинальном стандарте 98го года.
MountainGoat
06.06.2024 14:33Я знаю, что это очень древняя библиотека, но всё равно не могу избавиться от мысли, что это всё задумывалось как отчаянная попытка не переходить на Rust. :) Всё что в ней было годного давно или в std или в boost. А когда шаблон на шаблоне едет, шаблоном погоняет - потом отлаживать это то ещё удовольствие. Дебаггер в таких проектах просто показывает на рандомную строчку.
GlukKazan
06.06.2024 14:33+1Не было тогда ещё Раста. И D тоже пока ещё не было.
Выживали как могли...
ImagineTables
06.06.2024 14:33Принцип заключается в минимизации внутренних зависимостей между компонентами библиотеки. Каждый компонент может быть использован независимо!
Как же я не люблю спроектированные таким образом библиотеки. Если я тащу библиотеку в проект, у меня так или иначе появляется зависимость. Уплатив эту цену, я хочу все удобства связности. Это, конечно, относится именно к библиотекам, а не набору разрозненных компонентов, зачем-то собранных в одно целое.
zorn-v100500
06.06.2024 14:33Возьмем к примеру symfony (это их php)
Зачем мне тащить в проект компонент для обработки очередей, если мне надо отправлять почту ?
а не набору разрозненных компонентов, зачем-то собранных в одно целое.
Ну видимо для того чтобы потом их разделить ?
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);
А зачем передавать список типов в шаблон, если потом каждый из них все равно руками регистрировать?
Readme
06.06.2024 14:33+5Отвратительно, OTUS. Копирайтер откопал древнюю либу с кликбейтным названием, и попросил ChatGPT её кратко описать (отсюда и нестыковки по функциональности).
Тем не менее, сама идея и реализация Loki господином Александреску в своё время были весьма интересны, и во многом предвосхитили фичи будущего стандарта C++11. Если бы статья была обзором-ретроспективой с высоты текущих C++23/26, было бы круто. Но, видимо, перехочется — не в этой рекламной брошюрке.
wataru
06.06.2024 14:33и попросил ChatGPT её кратко описать (отсюда и нестыковки по функциональности).
Хм, а это весьма правдоподобное объяснение! Ясно, откуда тот же Invoke вылез, ибо ни гугл, ни бинг с яндексом нигде loki invoke ненаходят. Куски кода из примеров нигде не встречаются.
Keeper10
Александреску, перелогиньтесь.