如何有效地为现代 C++ 中指向虚拟基类的指针向量分配空间

Posted

技术标签:

【中文标题】如何有效地为现代 C++ 中指向虚拟基类的指针向量分配空间【英文标题】:How to efficiently allocate space for vector of pointers to virtual base class in modern C++ 【发布时间】:2015-06-19 08:01:08 【问题描述】:

我有以下数据模型

struct Base 
    int x_;
    int y_;
    int z_;

    virtual int getId() const;
    virtual int getValue() const = 0;
    virtual Base* create() const = 0;
    bool operator<(const Base &other);
;

struct Derived0 : public Base 
    virtual Derived0* create() const  return new Derived0(); ;
    virtual int getId() const;
    virtual int getValue() const;
;
//...
struct DerivedN : public Base 
    virtual DerivedN* create() const  return new DerivedN(); ;
    virtual int getId() const;
    virtual int getValue() const;
;

并按以下方式填写(简化)

int n = 0;
std::shared_ptr<Base> templ[100];

templ[n++] = std::make_shared<Derived0>();
//...
templ[n++] = std::make_shared<DerivedN>();

std::vector<std::shared_ptr<Base>> b;

for (int i = 0; i < n; i++) 
    while (...)  // hundreds of thousands iterations
        std::shared_ptr<Base> ptr(templ[i]->create());
        // previous call consumes most of the time
        //...
        b.push_back(ptr);
    


std::sort(b.begin(), b.end());
// ...

由于我需要大量派生对象,我想知道是否可以更有效地完成初始化。在展示的情况下,大部分时间都花在创建单个共享指针上。

我尝试了一种预先分配Base 对象数组的方法(因为所有Derived 具有相同的大小),为每个模板转换虚拟类型并存储指向该数组的原始指针。毫不奇怪,这种方法要快很多倍。 但是不干净,vector不能用,内存管理有问题。

谁能给我一个建议,如何以 C++ 方式有效地做到这一点

如果所有对象的大小都相同? 如果大小不同?

【问题讨论】:

这里看不到推导,怎么管理int create() new DerivedX(); :返回类型与返回值不对应。 代码被简化为无意义:我无法从中得出结论,因为有这么多错误,我如何区分“预期特征”和“错字”?仅通过文本的其余部分,此时为什么要阅读代码? ;) 你需要那个原型数组来做其他事情还是只是用它来填充向量? 代码中大约一半的行无法编译。 如果你使用 C++11,你可以调用带有大小的向量构造函数(在你的情况下是 n * 无论如何),所以它首先分配足够的内存 【参考方案1】:

在我看来,你的很多性能问题可以通过使用std::unique_ptr 并提前保留一些std::vector 内存来解决。

std::shared_ptr<Base> ptr(templ[i]->create());

上述行涉及为派生类型和std::shared_ptr 控制块动态分配内存。如果您没有共享所有权语义,则改用std::unique_ptr 将消除对其中一种分配的需要。

b.push_back(ptr);

当您执行上述足够次数时,向量将用完它为您分配的内存并尝试分配更多。 std::vector 的设计方式可以摊销恒定的时间复杂度,但我们可以采取任何措施来缓解这种情况,尤其是使用巨大的向量时,可以节省时间。

您的新代码可能类似于:

std::vector<std::unique_ptr<Base>> b;
b.reserve(n * /*number of iterations*/);

for (int i = 0; i < n; i++) 
    while (...)  // hundreds of thousands iterations
        std::unique_ptr<Base> ptr(templ[i]->create());
        //...
        b.push_back(ptr);
    

顺便说一句,您可以通过执行以下操作来限制创建原型数组的代码重复:

template <class Base, class... Derived, std::size_t... Idx>
auto arrayOfUniqueDerived (std::index_sequence<Idx...>)

    std::array<std::unique_ptr<Base>, sizeof...(Derived)> arr;
    (void) std::initializer_list<int>  (arr[Idx] = std::make_unique<Derived>(), 0)... ;
    return arr;


template <class Base, class... Derived>
auto arrayOfUniqueDerived ()

    return arrayOfUniqueDerived<Base,Derived...>(std::index_sequence_for<Derived...>);

然后像这样使用它:

std::array<std::unique_ptr<Base>,3> templ =
      arrayOfUniqueDerived<Base,Derived0,Derived1,Derived2>();

【讨论】:

我会检查unique_ptr 将如何提供帮助,谢谢。然而,大部分时间可以通过分配更大的Base 块来节省,而不是为每个我认为的调用 new。 在这种情况下,您可以查看小型对象分配器,例如来自 Loki 或 Boost.Pool 的分配器。【参考方案2】:

创建一个变体样式类型橡皮擦,使所有内容看起来像Base

template<class T>struct tagusing type=T;;

template<class Base, class...Derived>
struct poly 
  Base* get()
    return const_cast<Base*>( const_cast<poly const*>( this )->get() );
  
  Base const* get()const
    if (!ops) return nullptr;
    return ops->to_base(&raw);
  
  Base* operator->() return get(); 
  Base const* operator->()const return get(); 
  Base& operator*() return *get(); 
  Base const& operator*()const return *get(); 
  explicit operator bool()const return get(); 

  template<class T,class...Args,
    class=std::enable_if<
    /* T is one of Derived... */
    >
  >
  void emplace(tag<T>,Args&&...args)
    cleanup();
    ops=&ops_for<T>();
    new(&raw)T(std::forward<Args>(args)...);
          
  poly& operator=(poly const& o)
    if (this==&o)return *this;
    cleanup();
    if (!o->ops) return *this;
    o->ops.copy_ctor( &raw, &o.raw );
    ops=o->ops;
    return *this;
  
  poly& operator=(poly&&o)
    if (this==&o)return *this;
    cleanup();
    if (!o->ops) return *this;
    o->ops.move_ctor( &raw, &o.raw );
    ops=o->ops;
    return *this;
  

  poly(poly const& o)
    if (!o->ops)return;
    o->ops.copy_ctor(&raw,&o.raw);
    ops=o->ops;
  
  poly(poly&& o)
    if (!o->ops)return;
    o->ops.move_ctor(&raw,&o.raw);
    ops=o->ops;
  

private:
  void cleanup()
    if (ops) ops->dtor(&raw);
    ops=nullptr;
  
  struct erase_ops
    void(*copy_ctor)(void*lhs,void const*rhs);
    void(*move_ctor)(void*lhs,void*rhs);
    void(*dtor)(void*ptr);
    Base const*(*to_base)(void const*ptr);
  ;
  template<class D>
  static erase_ops const& ops_for()
    static erase_ops r=
      // ...
    ;
    return r;
  ;
  erase_ops const* ops=nullptr; // = &ops_for<Derived1>(); etc
  std::aligned_storage< /* size and alignment info */ > raw;
;

实施遗漏了,我正在打电话。

一旦你有了上面,你可以创建一个poly&lt;Base, Derived1, Derived2, ....的向量。成本是每个实例一个额外的指针。

现在我们已经复制了大部分虚拟调度,所以我们可以在类型擦除中包含DerivedN 上的剩余操作,这些操作被实现为虚拟方法并削减另一个指针的成本。如果Base 稍微大一点,我不会打扰。

C++ 喜欢值类型。给它想要的东西。

【讨论】:

以上是关于如何有效地为现代 C++ 中指向虚拟基类的指针向量分配空间的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中将指向基类的指针传递给派生类的成员函数

关于C++基类、派生类的引用和指针

C++“处理多个基类的虚拟函数”

指向派生对象的基类指针的 C++ 排序容器

在 C++ 中,如何获取指向向量的指针?

C++继承,发送一个指向基类的指针