【问题标题】:Inheritance-free polymorphism无继承多态性
【发布时间】:2013-03-06 19:13:48
【问题描述】:

X:我有一组不相关的类似容器的对象(矢量、地图、树……)处理不同不相关类型的对象。这些容器中对象的生命周期在它们的某些子集之间共享。我有一个对象负责他们的同步。我能想到的同步类的最简单实现是有一个BaseContainerLike 指针向量,其中BaseContainerLike 将是一个为我要管理的所有类容器对象实现公共接口的类。但并非所有人都像容器一样。它们可以像容器一样使用,但是让它们从一个通用的基类继承感觉很奇怪,我担心它会非常强烈地结合我的设计。

所以我创建了一个像这样的ContainerLikeInterface 类:

struct ContainerLikeInterface {
template<T>
ContainerLikeInterface(T& t) 
 : create([](int i){ return t->create(i); }),    // this is just an example
   delete([](int i){ return t->delete(i); }) {}

std::function<void(int)> create;
std::function<void(int)> delete;
};

template<class T>
ContainerLikeInterface make_containerLikeInterface(T& t) {
  return ContainerLikeInterface(t);
}

这使我能够以非侵入性的方式轻松创建接口向量(我可以为不同类型的构造函数部分专门化)。我使用这种方法的代码比使用继承时稍快,但它需要更多的内存和更长的编译时间(但我不优先考虑编译时间)。但是,我不知道这种方法是否能很好地适应我的项目。而且我读过一些关于价值语义的文章,其中人们更喜欢将对象所有权转移到接口,所以我有以下问题:

  • 这种方法的优缺点是什么?
  • 从长远来看,这会给我带来一些问题吗?
  • 我应该改用继承吗?
  • 我应该以不同的方式实现它吗?
  • 我应该改用库吗? (boost::TypeErasureadobe::polypyrtsa/poly

【问题讨论】:

  • 这是否是一件好事完全取决于您要解决的问题,而您已经忽略了。也就是说,你一开始为什么会出现这种情况?
  • @GManNickG 我添加了更多关于我的具体问题的信息,解释了我是如何陷入这种情况的。

标签: c++ boost c++11 delegates polymorphism


【解决方案1】:

您的基本想法(不需要继承)很好。我建议改用 Adob​​e.Poly。当您在每个单一操作中使用 1 个std::function 时,您有 N 种虚拟表(指针),以及可能的 N 堆分配(取决于是否可以应用 SBO(小缓冲区优化))。

您也很可能遇到对象生命周期管理问题。在您的实现中,您假设真实对象的寿命比“接口”长。迟早你会弄错的。这就是我鼓励采用value-semantic 方法的原因。 Adobe.Poly 为您提供。

使用 Adob​​e.Poly,您只能获得一个 vtable(指针)。它还实现了 SBO:可能不是单一分配。

我不一定会选择 Boost.TypeErasure。它需要学习另一种“语言”来指定接口,这种语言利用了大量的元编程,并且截至今天它还没有实现 SBO。

Adobe.Poly 没有很好的文档记录。有关如何使用它的示例,请参阅 this post。另外,请参阅this paper 了解它是如何实现的。

【讨论】:

  • 嘿,我在今年早些时候从您的帖子中了解到 Poly,感谢您撰写这些内容。但是,默认情况下,我不同意您对值语义的看法。我同意它更安全,但我认为哪个最好取决于应用程序。从性能的角度来看,您需要用于多态访问的引用语义,以及用于单态访问(和大腿循环)的值语义和数组容器。 OTOH,如果您正在处理,例如,一个 GUI 并且有小部件并且不关心小部件的确切类型,值语义可以简化所有权,因此更好。
  • 嗯。我当然同意您是否需要引用或值语义取决于应用程序的说法。我只是不确定性能是这种类型的“应用程序”。如果您已经衡量了使用价值语义方法与参考语义方法的成本,那么您最好进行判断。我使用引用语义的动机是当我需要从两个不同的位置访问同一个对象时(例如,因为我需要观察其状态的变化)。
【解决方案2】:

您的界面与在 Rust object system。经典的基于 VMT 的接口有一个指向对象的指针,该对象包含指向 VMT 的指针。你的有 2 个指针:一个指向对象,另一个指向方法表。它 [几乎] 看起来总是比具有您已经提到的缺点(内存使用等)的虚拟功能更强大 至于速度,std::function 使用标准分配器来保存指向t 的指针。如果你经常调用 ContainerLikeInterface 构造函数,它可能会导致性能下降,因为它需要在你的界面中为每个 std::function 至少分配一次,你可以为此编写自己的。

【讨论】:

  • 这取决于 std::function 的实现,见probablydance.com/2013/01/13/…
  • 感谢您提供有关 rust 的链接,它看起来真的很有趣!
  • 你是对的,调用 ContainerLikeInterface 可能会因为调用 malloc 而产生开销。我可以使用自定义池分配器,但在这种情况下,我认为它不值得,因为 ContainerLikeInterface 用于容器,而我没有那么多。感谢您的想法,我以后在编写这样的接口容器时会考虑它。
  • 感谢这种在 C++ 中的接口方式,我一定会使用它。除了分配所需的时间之外,堆(甚至池)分配还会减慢程序的速度,因为它碰巧比就地分配对缓存不友好。当一切都是本地的时,你显然需要更少的地方来存储在 CPU 缓存中。我从来没有测试过这个,只是读过。
【解决方案3】:

您实际上是在创建一个带有ContainerLikeInterface 接口的代理对象,该接口带有一个指向某些T 的指针或引用。

有一种方法可以避免创建代理,同时仍然使用无法派生ContainerLikeInterface 的标准容器。它可能被称为混合设计模式,并且在 C++11 中完美转发,它看起来像:

#include <vector>

struct IContainerLikeInterface
{
    virtual void create(int) = 0;
    virtual void erase(int) = 0;
};

template<class T>
struct ContainerLikeInterface : T, IContainerLikeInterface
{
    template<class... Args>
    ContainerLikeInterface(Args&& ...args) 
        : T(std::forward<Args>(args)...)
    {}

    // implement IContainerLikeInterface
    void create(int) override;
    void erase(int) override;
};

int main() {
    ContainerLikeInterface<std::vector<int> > v(10);
    v.size();
}

但是,它具有侵入性,因为所有有问题的容器声明,例如 std::vector&lt;int&gt;,都必须更改为 ContainerLikeInterface&lt;std::vector&lt;int&gt; &gt;。不过用法保持不变,因为ContainerLikeInterface&lt;T&gt; 是-a T

【讨论】:

  • 谢谢!但是当使用混合(用 CRTP 实现)时,每个接口都会有不同的类型。也就是说,您不能拥有接口向量。
  • @gnzlbg 哦,对不起,我第一次没有让你正确。答案已更新。
  • 感谢您更新答案。我的容器已经实现为来自基本容器类的 mixin(也使用 CRTP)。这是我在帖子中定义的最简单的替代方案。很酷的一点是,您可以“在可能的地方”使用静态多态性,并且仍然能够进行动态多态性。但是我的容器有很多非常小的内联函数。从我定义一个虚函数 gcc 停止内联所有内容的那一刻起,这就是切换到我现在正在做的事情的原因之一……要么切换到完整的运行时多态性……
猜你喜欢
  • 2013-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-03
相关资源
最近更新 更多