На этом этапе эта библиотека экспериментальна, и это чистое любопытство. Стабильность интерфейса или качества реализации не гарантирована. Используйте на своих собственных рисках.
Dyno решает проблему полиморфизма времени выполнения лучше, чем ванильный C ++. Он обеспечивает способ определить интерфейсы, которые могут быть выполнены неинтразивно, и обеспечивает полностью настраиваемый способ хранения полиморфных объектов и отправки в виртуальные методы. Это не требует наследства, распределения кучи или оставляющего комфортный мир семантики ценностей, и он может сделать это, превышая ванильный C ++.
Dyno -это реализация чистой библиотеки того, что также известно как объекты Rust-Trait, интерфейсы GO, классы типа Haskell и виртуальные концепции. Под капотом он использует технику C ++, известную как стирание типа, которая является идеей std::any , std::function и многие другие полезные типы.
# include < dyno.hpp >
# include < iostream >
using namespace dyno ::literals ;
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](T const & self, std::ostream& out) { self. draw (out); }
);
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}В качестве альтернативы, если вы обнаружите, что это слишком много шаблон, и вы можете стоять, используя макрос, следующее является эквивалентным:
# include < dyno.hpp >
# include < iostream >
// Define the interface of something that can be drawn
DYNO_INTERFACE (Drawable,
(draw, void (std::ostream&) const )
);
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (Drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}Это библиотека C ++ 17. Не будет предпринято никаких усилий для поддержки более старых компиляторов (извините). Известно, что библиотека работает со следующими компиляторами:
| Компилятор | Версия |
|---|---|
| GCC | > = 7 |
| Герметичный | > = 4,0 |
| Яблочный кланг | > = 9,1 |
Библиотека зависит от Boost.hana и Boost.callabletraits. Единисты зависят от либаутных, а контрольные показатели зависят от Benchmark, Boost.TypeerAsure и Mpark.variant, но вам не нужно, чтобы они использовали библиотеку. Для локальной разработки сценарий dependencies/install.sh может использоваться для автоматической установки всех зависимостей.
Dyno -это библиотека только для заголовка, поэтому нечего строить на первую очередь. Просто добавьте include/ Directory в путь поиска заголовка вашего компилятора (и убедитесь, что зависимости удовлетворены), и все готово. Тем не менее, есть модульные тесты, примеры и тесты, которые могут быть построены:
(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH= " ${PWD} /../dependencies/install " ) # Setup the build directory
cmake --build build --target examples # Build and run the examples
cmake --build build --target tests # Build and run the unit tests
cmake --build build --target check # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks При программировании необходимость манипулировать объектами с общим интерфейсом, но с другим динамическим типом возникает очень часто. C ++ решает это с наследством:
struct Drawable {
virtual void draw (std::ostream& out) const = 0;
};
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
struct Circle : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
void f (Drawable const * drawable) {
drawable-> draw (std::cout);
}Однако этот подход имеет несколько недостатков. Это
Навязчивый
Для того, чтобы Square и Circle выполняли Drawable , оба они должны унаследовать от Drawable класса. Это требует наличия лицензии на изменение этих классов, что делает наследство очень необоснованным. Например, как бы вы сделали std::vector<int> выполнить интерфейс Drawable ? Вы просто не можете.
Несовместимая с семантикой стоимости
Наследование требует, чтобы вы передали полиморфные указатели или ссылки на объекты вместо самих объектов, которые очень плохо играют с остальной частью языка и стандартной библиотеки. Например, как бы вы скопировали вектор с Drawable S? Вам нужно предоставить метод виртуального clone() , но теперь вы просто испортили свой интерфейс.
Тесно связано с динамическим хранением
Из -за отсутствия семантики ценности мы обычно в конечном итоге распределяем эти полиморфные объекты на кучу. Это ужасно неэффективно и семантически неправильно, поскольку скорее всего, нам вообще не понадобилась продолжительность динамического хранения, и объект с автоматической продолжительностью хранения (например, в стеке) было бы достаточно.
Предотвращает внедрение
В 95% случаев мы в конечном итоге вызываем виртуальный метод с помощью полиморфного указателя или ссылки. Это требует трех заряда: одна для загрузки указателя в VTable внутри объекта, один для загрузки правильной записи в VTable, и один для косвенного вызова к указателю функции. Все это прыжки вокруг затрудняют компилятор, чтобы принимать хорошие решения для внедрения. Тем не менее, оказывается, что все эти обороты, кроме косвенного вызова, можно избежать.
К сожалению, это выбор, который C ++ сделал для нас, и это правила, с которыми мы связаны, когда нам нужен динамический полиморфизм. Или это действительно?
Dyno решает проблему полиморфизма времени выполнения в C ++ без каких -либо недостатков, перечисленных выше, и многих других вкусностей. Это:
Несвязанно
Интерфейс может быть выполнен типом, не требуя никакой модификации этого типа. Черт, тип может даже выполнять один и тот же интерфейс по -разному! С Dyno вы можете поцеловать нелепые классовые иерархии до свидания.
100% на основе семантики стоимости
Полиморфные объекты могут быть переданы как есть с их естественной семантикой. Вам нужно скопировать свои полиморфные объекты? Конечно, просто убедитесь, что у них есть конструктор копирования. Вы хотите убедиться, что они не копируются? Конечно, отметьте это как удаленное. С Dyno глупые методы clone() и пролиферация указателей в API являются вещами прошлого.
Не в сочетании с какой -либо конкретной стратегией хранения
То, как хранится полиморфный объект, действительно является детализацией реализации, и он не должен мешать тому, как вы используете этот объект. Dyno дает вам полный контроль над тем, как хранятся ваши объекты. У вас много маленьких полиморфных объектов? Конечно, давайте храним их в местном буфере и избегаем любого распределения. Или, может быть, для вас имеет смысл хранить вещи в куче? Конечно, продолжай.
Гибкий механизм отправки для достижения наилучшей возможной производительности
Хранение указателя на VTable - это лишь одна из многих различных стратегий реализации для выполнения динамической диспетчеры. Dyno дает вам полный контроль над тем, как происходит динамическая отправка, и в некоторых случаях может побить VTables. Если у вас есть функция, которая называется в горячей петле, вы можете, например, сохранить ее непосредственно в объекте и пропустить корабли VTABLE. Вы также можете использовать знания, специфичные для приложения. Компилятор никогда не должен был оптимизировать некоторые динамические вызовы-девиртуализацию на уровне библиотеки.
Во -первых, вы начинаете с определения общего интерфейса и давая ему имя. Dyno предоставляет простой язык для этого. Например, давайте определим интерфейс, который Drawable , который описывает типы, которые можно нарисовать:
# include < dyno.hpp >
using namespace dyno ::literals ;
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { }; Это определяет Drawable как представляющий интерфейс для всего, у которого есть метод, который называется draw , принимая ссылку на std::ostream . Dyno называет эти интерфейсы динамические концепции , поскольку они описывают наборы требований, которые должны выполняться типом (например, концепции C ++). Однако, в отличие от концепций C ++, эти динамические концепции используются для генерации интерфейсов времени выполнения, отсюда и название динамическое . Приведенное выше определение в основном эквивалентно следующему:
struct Drawable {
virtual void draw (std::ostream&) const = 0;
};После определения интерфейса следующим шагом является создание типа, который удовлетворяет этому интерфейсу. С наследством вы бы написали что -то вроде этого:
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final {
out << " square " << std::endl;
}
};С Dyno полиморфизм не является интрузивным и вместо этого предоставляется через то, что называется концептуальной картой (после C ++ 0x концептуальные карты):
struct Square { /* ... */ };
template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
" draw " _s = [](Square const & square, std::ostream& out) {
out << " square " << std::endl;
}
);Эта конструкция является специализацией шаблона переменной C ++ 14 с именем
concept_map, определенного вdyno::names. Затем мы инициализируем эту специализацию с помощьюdyno::make_concept_map(...).
Первый параметр лямбды - это неявный *this параметр, который подразумевается, когда мы объявляем draw как метод выше. Также возможно стереть функции, не являющиеся членами (см. Соответствующий раздел).
Эта концептуальная карта определяет, как тип Square удовлетворяет концепции Drawable . В некотором смысле, он отображает типовой Square с его реализацией концепции, которая мотивирует наименование. Когда тип удовлетворяет требованиям концепции, мы говорим, что типовые модели (или являются моделью) этой концепцией. Теперь, когда Square является моделью концепции Drawable , мы хотели бы использовать Square полиморфическую роль в качестве Drawable . С традиционным наследством мы использовали бы указатель на базовый класс, как это:
void f (Drawable const * d) {
d-> draw (std::cout);
}
f ( new Square{}); С Dyno полиморфизм и ценностная семантика совместима, и способ, которым полиморфные типы могут быть высоко настроены. Чтобы сделать это, нам нужно определить тип, который может удерживать все, что можно Drawable . Именно этот тип, вместо того, чтобы Drawable* , мы будем передаваться в полиморфные функции и обратно. Чтобы помочь определить эту обертку, Dyno предоставляет контейнер dyno::poly , который может удерживать произвольный объект, удовлетворяющий данной концепции. Как вы увидите, dyno::poly играет двойную роль: он хранит полиморфический объект и заботится о динамической диспетчеринге методов. Все, что вам нужно сделать, это написать тонкую обертку над dyno::poly , чтобы дать ему именно желаемый интерфейс:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};Примечание: технически вы можете использовать
dyno::polyнапрямую в своих интерфейсах. Тем не менее, гораздо удобнее использовать обертку с реальными методами, чемdyno::poly, и поэтому рекомендуется написание обертки.
Давайте разберем это. Во -первых, мы определяем элемент poly_ , который является полиморфным контейнером для всего, что моделирует концепцию Drawable :
dyno::poly<Drawable> poly_; Затем мы определяем конструктор, который позволяет построить этот контейнер из произвольного типа T :
template < typename T>
drawable (T x) : poly_{x} { } Невысленное предположение здесь заключается в том, что T фактически моделирует концепцию Drawable . Действительно, когда вы создаете dyno::poly из объекта Type T , Dyno пойдет и посмотрите на концептуальную карту, определенную для Drawable и T , если таковые имеются. Если нет такой концептуальной карты, библиотека сообщит, что мы пытаемся создать dyno::poly из типа, который не поддерживает ее, и ваша программа не компилируется.
Наконец, самая странная и самая важная часть приведенного выше определения - это метод draw :
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); } Здесь происходит то, что когда .draw вызывает на нашем объекте drawable , мы на самом деле выполним динамическую отправку для реализации функции "draw" для объекта, хранящегося в настоящее время в dyno::poly , и вызовут это. Теперь, чтобы создать функцию, которая принимает все, что Drawable , больше не нужно беспокоиться о указателях и владении в вашем интерфейсе:
void f (drawable d) {
d. draw (std::cout);
}
f (Square{});Кстати, если вы думаете, что это все глупо, и вы должны были использовать шаблон, вы правы. Тем не менее, рассмотрим следующее, где вам действительно нужен полиморфизм времени выполнения :
drawable get_drawable () {
if ( some_user_input ())
return Square{};
else
return Circle{};
}
f (get_drawable()); Строго говоря, вам не нужно обернуть dyno::poly , но это ставит хороший барьер между Dyno и остальной частью вашего кода, который никогда не должен беспокоиться о том, как реализуется ваш полиморфный слой. Кроме того, мы в значительной степени проигнорировали, как dyno::poly был реализован в приведенном выше определении. Тем не менее, dyno::poly является очень мощным политическим контейнером для полиморфных объектов, который может быть настроен на потребности в производительности. Создание обертки drawable позволяет легко настроить стратегию реализации, используемую dyno::poly для производительности, не влияя на остальную часть вашего кода.
Первый аспект, который можно настроить в dyno::poly - это то, как объект хранится в контейнере. По умолчанию мы просто храним указатель на фактический объект, как это было бы с полиморфизмом на основе наследования. Тем не менее, это часто не самая эффективная реализация, и именно поэтому dyno::poly позволяет настраивать ее. Для этого просто передайте политику хранения dyno::poly . Например, давайте определим нашу drawable , чтобы она пыталась хранить объекты до 16 байтов в локальном буфере, но затем возвращается к куче, если объект больше:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable, dyno::sbo_storage< 16 >> poly_;
// ^^^^^^^^^^^^^^^^^^^^^ storage policy
}; Обратите внимание, что ничего, кроме политики, не изменилось в нашем определении. Это один очень важный принцип динамо ; Эти политики являются деталями реализации, и они не должны менять способ писать свой код. С приведенным выше определением вы теперь можете создавать drawable S, как и раньше, и никакого распределения не произойдет, когда объект, который вы создаете, drawable из подгонки в 16 байт. Однако, когда это не подходит, dyno::poly выделяет достаточно большой буфер на кучу.
Допустим, вы на самом деле никогда не хотите делать распределение. Нет проблем, просто измените политику на dyno::local_storage<16> . Если вы попытаетесь построить drawable из объекта, который слишком велик, чтобы соответствовать локальному хранилищу, ваша программа не будет компилироваться. Мы не только сохраняем распределение, но мы также сохраняем корабли указателя каждый раз, когда получаем доступ к полиморфическому объекту, если сравниваемся с традиционным подходом на основе наследования. Настраивая эти (важные) детали реализации для вас конкретного варианта использования, вы можете сделать свою программу гораздо более эффективной, чем при классическом наследстве.
Другие политики хранения также предоставляются, такие как dyno::remote_storage и dyno::non_owning_storage . dyno::remote_storage -это по умолчанию, который всегда хранит указатель на объект, выделяемый кучей. dyno::non_owning_storage хранит указатель на объект, который уже существует, не беспокоясь о жизни этого объекта. Это позволяет реализовать необладающие полиморфные представления по объектам, что очень полезно.
Политики на заказ также могут быть созданы довольно легко. См. <dyno/storage.hpp> для получения подробной информации.
Когда мы представили dyno::poly , мы упомянули, что у него было две роли; Во -первых, хранить полиморфный объект, а второй - выполнить динамическую диспетчери. Так же, как хранилище можно настроить, способ выполнения динамической диспетчеризации также можно настроить с использованием политик. Например, давайте определим нашу drawable , чтобы вместо хранения указателя на vtable она вместо этого хранит vtable в самом объекте drawable . Таким образом, мы избегаем одного косвенного направления каждый раз, когда мы получаем доступ к виртуальной функции:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >; // storage policy
using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
dyno::poly<Drawable, Storage, VTable> poly_;
}; Обратите внимание, что ничто, кроме политики VTAble, не должна измениться в определении нашего типа drawable . Кроме того, если бы мы хотели, мы могли бы изменить политику хранения независимо от политики VTAble. С помощью вышесказанного, даже если мы сохраняем все расходы, мы платим за это, увеличивая наше объект drawable больше (поскольку он должен держать VTAble на локальном уровне). Это могло бы быть запретительным, если бы у нас было много функций в VTable. Вместо этого было бы больше смысла хранить большую часть удаленного VTAble, но только внедрять те несколько функций, которые мы называем сильно. Dyno делает это очень легко сделать это с помощью селекторов , которые можно использовать для настройки, к каким функциям применяется политика:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >;
using VTable = dyno::vtable<
dyno::local<dyno::only<decltype( " draw " _s)>>,
dyno::remote<dyno::everything_else>
>;
dyno::poly<Drawable, Storage, VTable> poly_;
}; Учитывая это определение, VTable фактически разделен на два. Первая часть локально для объекта drawable и содержит только метод draw . Вторая часть - это указатель на VTable в статическом хранилище, которое содержит оставшиеся методы (например, деструктор).
Dyno предоставляет две политики Vtable, dyno::local<> и dyno::remote<> . Обе эти политики должны быть настроены с использованием селектора . Селекторы, поддерживаемые библиотекой, - это dyno::only<functions...> , dyno::except<...> и dyno::everything_else (который также может быть написан dyno::everything ).
При определении концепции часто бывает тот случай, когда можно предоставить определение по умолчанию, по крайней мере, для некоторых функций, связанных с концепцией. Например, по умолчанию, вероятно, имеет смысл использовать функцию -член с именем draw (если есть) для реализации метода абстрактного "draw" концепции Drawable . Для этого можно использовать dyno::default_concept_map :
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = []( auto const & self, std::ostream& out) { self. draw (out); }
); Теперь, когда мы пытаемся посмотреть, как какой -то тип T выполняет концепцию Drawable , мы вернемся к концептуальной карте по умолчанию, если не была определена карта концепции. Например, мы можем создать новый Circle типа:
struct Circle {
void draw (std::ostream& out) const {
out << " circle " << std::endl;
}
};
f (Circle{}); // prints "circle" Circle является автоматически моделью Drawable , хотя мы явно не определили концептуальную карту для Circle . С другой стороны, если бы мы определили такую концептуальную карту, она имела бы приоритет над по умолчанию:
template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
" draw " _s = [](Circle const & circle, std::ostream& out) {
out << " triangle " << std::endl;
}
);
f (Circle{}); // prints "triangle" Иногда полезно определить концептуальную карту для полного семейства типов одновременно. Например, мы могли бы захотеть сделать std::vector<T> модель Drawable , но только тогда, когда T можно напечатать в поток. Это легко достигнуто с помощью этого (не так) секретного трюка:
template < typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std:: void_t <decltype(
std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
" draw " _s = [](std::vector<T> const & v, std::ostream& out) {
for ( auto const & x : v)
out << x << ' ' ;
}
);
f (std::vector< int >{ 1 , 2 , 3 }) // prints "1 2 3 "Обратите внимание на то, как нам вообще не нужно изменять
std::vector. Как мы могли бы сделать это с помощью классического полиморфизма? Ответ: нет.
Dyno позволяет стирать не членами функции и функции, которые отправляются в произвольном аргументе (но только один аргумент). Чтобы сделать это, просто определите концепцию, используя dyno::function вместо dyno::method и используйте dyno::T Заполнитель, чтобы обозначить аргумент, стерщенный:
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (dyno::T const &, std::ostream&)>
)) { }; dyno::T const& Parameter, используемый выше, представляет тип объекта, на котором вызывается функция. Тем не менее, это не обязательно должен быть первый параметр:
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (std::ostream&, dyno::T const &)>
)) { };Выполнение концепции не изменяется, использует ли концепция метод или функцию, но убедитесь, что параметры реализации вашей функции соответствуют функции, объявленной в концепции:
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](std::ostream& out, T const & self) { self. draw (out); }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
); Наконец, при вызове function на dyno::poly вам придется явно пройти во все параметры, так как Dyno не может догадаться, на каком вы хотите отправить. Параметр, который был объявлен с заполнителем dyno::T в концепции, должен пройти dyno::poly :
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out, poly_); }
// ^^^^^ passing the poly explicitly
private:
dyno::poly<Drawable> poly_;
};