Локи в C++: библиотека для проектирования и метапрограммирования
Привет, Хабр!
Сегодня мы рассмотрим библиотеку Loki, которая названа в честь одного из самых интересных и хитроумных богов скандинавской мифологии. Как и его мифологический тёзка, Loki в C++ отличается гибкостью и изяществом, имея мощные инструменты для метапрограммирования и реализации паттернов проектирования.
Принципы Loki
Малое прекрасно:
Принцип заключается в минимизации внутренних зависимостей между компонентами библиотеки. Каждый компонент может быть использован независимо!
Например, существуют такие компоненты, как SmartPointer или TypeList, они могут быть использованы отдельно друг от друга.
Мультипликативное превосходство:
Минимальные зависимости:
Loki делает минимальные предположения о своем окружении и предоставляет гибкие точки расширения, чтобы можно было адаптировать библиотеку под нужды без необходимости вносить изменения в саму либу.
Основные компоненты Loki
Типы данных
TypeList — это метапрограммная структура данных, которая представляет собой список типов. Используют для манипуляций с типами на этапе компиляции.
Можно выполнять различные операции с TypeList: доступ к первому элементу, добавление и удаление элементов.
Например, доступ к первому элементу осуществляется с помощью front
:
template
struct Front;
template
struct Front> {
using Type = Head;
};
using FirstType = Front::Type; // int
Добавление элемента в начало списка с помощью PushFront
:
template
struct PushFront;
template
struct PushFront, NewType> {
using Type = TypeList;
};
using NewList = PushFront::Type; // TypeList
template
struct PushFront;
template
struct PushFront, NewType> {
using Type = TypeList;
};
using NewList = PushFront::Type; // TypeList
Еще существует NullType
, который используется для обозначения конца списка типов или отсутствия типа.
struct NullType {};
using EmptyList = TypeList<>;
Умные указатели
Умные указатели в Loki представляют из себя шаблоны для управления ресурсами и автоматического освобождения памяти:
#include
using namespace Loki;
typedef SmartPtr IntPtr;
void example() {
IntPtr p(new int(10));
// автоматическое освобождение памяти при выходе из области видимости p
}
Мультиметоды
Мультиметоды позволяют реализовать множественную диспетчеризацию в C++. Так можно вызывать различные функции в зависимости от типов аргументов во время выполнения:
#include
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
using namespace Loki;
void print(int x) {
std::cout << x << std::endl;
}
int main() {
Functor f(print);
f(10); // Выведет 10
}
Паттерны проектирования: одиночка, фабрика, посетитель
Есть реализация паттерна одиночка с возможностью управления временем жизни объекта:
#include
using namespace Loki;
class MySingleton {
public:
void DoSomething() { /* ... */ }
};
int main() {
MySingleton& instance = Singleton::Instance();
instance.DoSomething();
}
Фабрика позволяет создавать объекты различных классов, не зная их точных типов:
#include
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 MyFactory;
MyFactory factory;
factory.Register(1);
std::unique_ptr obj(factory.CreateObject(1));
obj->DoSomething(); // Выведет "Derived"
}
Посетитель позволяет добавлять новые операции к существующим объектам без изменения их классов:
#include
using namespace Loki;
class Element {
public:
virtual void Accept(Visitor& visitor) = 0;
};
class ConcreteElement : public Element {
public:
void Accept(Visitor& visitor) override {
visitor.Visit(*this);
}
};
class ConcreteVisitor : public Visitor {
public:
void Visit(ConcreteElement& element) {
std::cout << "Visited ConcreteElement" << std::endl;
}
};
int main() {
ConcreteElement elem;
ConcreteVisitor visitor;
elem.Accept(visitor); // выведет "Visited ConcreteElement"
}
Политика-ориентированный дизайн
Политика-ориентированный дизайн позволяет создавать классы, которые могут быть настроены с помощью шаблонных параметров. Достигается за счет разбиения поведения класса на несколько политик, каждая из которых реализует определенную функциональность:
template
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 MySingleton;
Примеры использования
TypeList для динамического создания объектов:
#include
#include
#include
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 MyFactory;
int main() {
MyFactory factory;
factory.Register(1);
factory.Register(2);
factory.Register(3);
std::unique_ptr a(factory.CreateObject(1));
std::unique_ptr b(factory.CreateObject(2));
std::unique_ptr c(factory.CreateObject(3));
a->Display();
b->Display();
c->Display();
return 0;
}
Так можно динамически создавать объекты различных типов, используя TypeList и фабрику.
Управление памятью с помощью SmartPtr
:
#include
#include
using namespace Loki;
class Resource {
public:
Resource() { std::cout << "Resource Acquired\n"; }
~Resource() { std::cout << "Resource Released\n"; }
};
void useResource() {
SmartPtr res(new Resource());
// ресурс автоматом освободится в конце блока
}
int main() {
useResource();
return 0;
}
Реализация многометодного диспетчера:
#include
#include
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
#include
using namespace Loki;
void PrintNumber(int number) {
std::cout << "Number: " << number << std::endl;
}
int main() {
Functor func(PrintNumber);
func(42);
return 0;
}
Подробнее с библиотекой можно ознакомиться здесь.
В завершение хочу рассказать об открытых уроках для разработчиков на C++, которые совсем скоро пройдут в Otus:
11 июня: Условные переменные в С++. Узнаете, что такое std: condition_variable, какие задачи он решает и типовые ошибки при его использовании. Узнаете, что такое spurious wakeup и напишите несколько concurrency-примитивов на основе condition_variable. Записаться бесплатно можно по ссылке.
24 июня: Как разработчику на С++ организовать кроссплатформенную разработку? На этом уроке узнаете, как решить проблему поиска зависимостей, напишите conan-файл и сможете организовать свой сервер пакетов в своей экосистеме CI/CD. Запись по ссылке.