在这一点上,该库是实验性的,它是一种纯粹的好奇心。不能保证接口或实施质量的稳定性。出于自己的风险使用。
Dyno比香草C ++更好地解决了运行时多态性的问题。它提供了一种定义可以非侵入性实现的接口的方法,并提供了一种完全可定制的方式来存储多态对象并将其分配到虚拟方法。它不需要继承,分配或留下舒适的价值语义世界,并且可以在表现优于香草C ++的同时这样做。
Dyno是纯净的纯净实现,也称为Rust特质对象,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库。不会努力支持较老的编译器(对不起)。众所周知,该图书馆可以与以下编译器合作:
| 编译器 | 版本 |
|---|---|
| 海湾合作委员会 | > = 7 |
| 铛 | > = 4.0 |
| Apple Clang | > = 9.1 |
该库取决于boost.hana和boost.callabletraters。单位测试取决于Libawful,基准测试取决于Google基准测试,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中,另一个用于向功能指针进行间接调用。所有这些跳动都使编译器难以做出良好的内在决定。但是,事实证明,除了间接呼叫以外,所有这些间接都可以避免。
不幸的是,这是C ++为我们做出的选择,这些是我们需要动态多态性的规则。还是真的?
Dyno解决了C ++中运行时多态性的问题,而没有上面列出的任何缺点,还有更多的好东西。这是:
非侵入性
可以通过类型实现接口,而无需对该类型进行任何修改。哎呀,一种类型甚至可以以不同的方式实现相同的接口!与Dyno一起,您可以亲吻荒谬的班级等级结构。
100%基于价值语义
可以通过其自然价值语义将多态对象传递。您需要复制多态对象吗?当然,只需确保它们具有复制构造函数即可。您想确保他们不会复制吗?当然,将其标记为已删除。使用Dyno ,愚蠢的clone()方法和API中指针的扩散是过去的事物。
不与任何特定的存储策略相结合
多态对象存储的方式实际上是一个实现细节,并且不应干扰您使用该对象的方式。 Dyno使您可以完全控制对象存储的方式。您有很多小的多态物体吗?当然,让我们将它们存储在本地缓冲区中,并避免任何分配。还是您可以将东西存储在堆上是有意义的?当然,继续。
灵活的调度机制以实现最佳性能
存储指向VTable的指针只是执行动态调度的许多不同实现策略之一。 Dyno可以完全控制动态调度的发生方式,实际上可以在某些情况下击败VTABLES。如果您的功能在热循环中调用,则可以直接将其存储在对象中并跳过VTable间接方向。您还可以使用特定于应用程序的知识,编译器永远不必优化某些动态调用 - 图书馆级的Devirtualization。
首先,您首先定义通用接口并给它命名。 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::名称空间中定义的concept_map。然后,我们使用dyno::make_concept_map(...)初始化该专业化。
lambda的第一个参数是隐式*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概念进行了建模。的确,当您从T型对象创建dyno::poly时, Dyno会去看为Drawable和T所定义的概念图(如果有)。如果没有这样的概念图,库将报告我们正在尝试从不支持它的类型中创建dyno::poly ,并且您的程序不会编译。
最后,上述定义中最奇怪,最重要的部分是draw方法:
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }这里发生的事情是,当.draw被调用我们的drawable对象时,我们实际上将对当前存储在dyno::poly中的对象的"draw"函数执行动态调度。现在,要创建一个可以接受任何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
};请注意,除了我们定义中的策略之外,没有什么。那是Dyno的一个非常重要的宗旨。这些策略是实施详细信息,它们不应更改您编写代码的方式。通过上述定义,您现在可以像以前一样创建drawable S,并且当您要从16字节中绘制的对象创建drawable对象时,不会发生分配。但是,当它不合适时, 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存储在drawable对象本身中,而不是存储指向VTable的指针。这样,每次访问虚拟功能时,我们都会避免一个间接:
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的成员函数(如果有)实现Drawable概念的抽象"draw"方法可能是有意义的。为此,可以使用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的概念图, Circle也是Drawable的模型。另一方面,如果我们要定义这样的概念图,则它将比默认一个优先:
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&参数上面使用的代表调用函数的对象的类型。但是,它不必是第一个参数:
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
);最后,当在dyno::poly上调用function时,您必须明确传递所有参数,因为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_;
};