多态对象数组

12
我经常需要创建多态对象的数组或向量。我通常更喜欢使用基类的引用而不是智能指针,因为它们往往更简单。
由于禁止使用原始引用来包含数组和向量,所以我倾向于使用智能指针来代替基类。然而,也有选择使用`std::reference_wrapper`代替:https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper 从文档中可以看出,这是其既定用途之一,但当涉及到包含多态对象的数组时,常见建议似乎是使用智能指针而不是`std::reference_wrapper`。
我的唯一想法是智能指针可能能够更好地处理对象的生存期?
简而言之,为什么在创建多态对象数组时,智能指针(如`std::unique_ptr`)似乎优于`std::reference_wrapper`?

8
如果对象是在其他地方拥有的,reference_wrapper 可以使用。然而,如果容器拥有这些对象并控制它们的生命周期,你必须使用智能指针如 unique_ptr - reference_wrapper 不提供任何所有权语义。 - BoBTFish
4
unique_ptr 管理所拥有对象的生命周期,reference_wrapper 存储指向该对象的指针。使用 unique_ptr 来保证对象的释放! - Alberto Miola
4
参考文献只是一个参考,仍然需要有人来管理对象的生命周期。使用智能指针可以一次性获得所有所需的内容。 - 463035818_is_not_a_number
谢谢大家,这很有道理。 - user7119460
2
实际上,你已经在你的问题中得到了答案。唯一需要补充的是这不是关于更加"整洁",而是引用根本不处理生命周期。 - 463035818_is_not_a_number
1
std::reference_wrapper 作为容器元素类型与普通的指针相比并没有任何优势,除了引用包装器不能为 null。 - n. m.
4个回答

14

简单来说:

  • unique_ptr 是对象的所有者。它管理所拥有对象的生命周期。

  • reference_wrapper 包装了指向内存中对象的指针。它不会管理所包装对象的生命周期。

当对象不再需要时,您应该创建一个unique_ptr(或 shared_ptr)数组以确保释放对象。


1
尝试在处理位于堆中的对象时始终使用智能指针。当我需要处理多态性时,我像你一样使用指针,但是我总是尽量不使用指针! - Alberto Miola

4

如果您足够有动力,可以编写一个poly_any<Base>类型。

poly_any<Base>any的一种形式,仅限于存储从Base派生出来的对象,并提供一个.base()方法返回基础对象的Base&

非常简略的草图:

template<class Base>
struct poly_any:private std::any
{
  using std::any::reset;
  using std::any::has_value;
  using std::any::type;

  poly_any( poly_any const& ) = default;
  poly_any& operator=( poly_any const& ) = default;

  Base& base() { return get_base(*this); }
  Base const& base() const { return const_cast<Base const&>(get_base(const_cast<poly_any&>(*this))); }

  template< class ValueType,
    std::enable_if_t< /* todo */, bool > =true
  >
  poly_any( ValueType&& value ); // todo

  // TODO: sfinae on ValueType?
  template< class ValueType, class... Args >
  explicit poly_any( std::in_place_type_t<ValueType>, Args&&... args );  // todo

  // TODO: sfinae on ValueType?
  template< class ValueType, class U, class... Args >
  explicit poly_any( std::in_place_type_t<ValueType>, std::initializer_list<U> il,
          Args&&... args ); // todo

  void swap( poly_any& other ) {
    static_cast<std::any&>(*this).swap(other);
    std::swap( get_base, other.get_base );
  }

  poly_any( poly_any&& o ); // todo
  poly_any& operator=( poly_any&& o ); // todo

  template<class ValueType, class...Ts>
  std::decay_t<ValueType>& emplace( Ts&&... ); // todo
  template<class ValueType, class U, class...Ts>
  std::decay_t<ValueType>& emplace( std::initializer_list<U>, Ts&&... ); // todo
private:
  using to_base = Base&(*)(std::any&);
  to_base get_base = 0;
};

然后,您只需拦截将任何东西放入poly_any<Base>的每种方法,并存储get_base函数指针:

template<class Base, class Derived>
auto any_to_base = +[](std::any& in)->Base& {
  return std::any_cast<Derived&>(in);
};

一旦您完成这个步骤,您可以创建一个std::vector<poly_any<Base>>,它是一个由从Base多态继承而来的值类型向量。
请注意,std::any通常使用小缓冲区优化来内部存储小对象,并将较大的对象存储在堆上。但这只是一个实现细节。

1

让我们尝试这个实验:

#include <iostream>
#include <vector>
#include <memory>
#include <functional>

class Base {
public:
   Base() {
     std::cout << "Base::Base()" << std::endl;
   }

   virtual ~Base() {
     std::cout << "Base::~Base()" << std::endl;
   }
};

class Derived: public Base {
public:
   Derived() {
     std::cout << "Derived::Derived()" << std::endl;
   }

   virtual ~Derived() {
     std::cout << "Derived::~Derived()" << std::endl;
   }
};

typedef std::vector<std::reference_wrapper<Base> > vector_ref;
typedef std::vector<std::shared_ptr<Base> > vector_shared;
typedef std::vector<std::unique_ptr<Base> > vector_unique;

void fill_ref(vector_ref &v) {
    Derived d;
    v.push_back(d);
}

void fill_shared(vector_shared &v) {
    std::shared_ptr<Derived> d=std::make_shared<Derived>();
    v.push_back(d);
}

void fill_unique(vector_unique &v) {
    std::unique_ptr<Derived> d(new Derived());
    v.push_back(std::move(d));
}

int main(int argc,char **argv) {

   for(int i=1;i<argc;i++) {
      if(std::string(argv[i])=="ref") {
    std::cout << "vector" << std::endl;
    vector_ref v;
        fill_ref(v);
    std::cout << "~vector" << std::endl;
      } else if (std::string(argv[i])=="shared") {
    std::cout << "vector" << std::endl;
    vector_shared v;
    fill_shared(v);
    std::cout << "~vector" << std::endl;
      } else if (std::string(argv[i])=="unique") {
    std::cout << "vector" << std::endl;
    vector_unique v;
    fill_unique(v); 
    std::cout << "~vector" << std::endl;
      }
   }
}

运行时使用共享参数:
vector
Base::Base()
Derived::Derived()
~vector
Derived::~Derived()
Base::~Base()

运行带有参数 unique

vector
Base::Base()
Derived::Derived()
~vector
Derived::~Derived()
Base::~Base()

使用参数 ref 运行

vector
Base::Base()
Derived::Derived()
Derived::~Derived()
Base::~Base()
~vector

说明:

  • shared: 内存被代码的不同部分共享。在示例中,Derived 对象首先由 d 局部变量和向量拥有。当代码退出函数范围时,对象仍然由向量拥有,只有当向量最终消失时,对象才被删除。
  • unique: 内存由 unique_ptr 拥有。在示例中,Derived 对象首先由 d 局部变量拥有。但是,它必须被 移动 到向量中,转让所有权。与之前一样,当唯一拥有者消失时,对象被删除。
  • ref: 没有拥有语义。对象作为 fill_ref() 函数的局部变量创建,对象的引用可以添加到向量中。但是,向量不拥有内存,当代码退出 fill_ref() 函数时,对象消失,使向量指向未分配的内存。

1

基本上,reference_wrapper 是一个可变的引用:像引用一样,它不能为 null;但像指针一样,在其生命周期内可以将其分配给另一个对象。

然而,像指针和引用一样,reference_wrapper 不管理对象的生命周期。这就是我们使用 vector<uniq_ptr<>>vector<shared_ptr<>> 的原因:确保引用的对象被正确处理。

从性能的角度来看,vector<reference_wrapper<T>> 应该与 vector<T*> 一样快速和内存有效。但是,这两个指针/引用可能会变得悬空,因为它们没有管理对象的生命周期。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接