13

I commonly come across the need to create arrays or vectors of polymorphic objects. I'd usually prefer to use references, rather than smart pointers, to the base class because they tend to be simpler.

Arrays and vectors are forbidden from containing raw references, and so I've tended to use smart pointers to the base classes instead. However, there is also the option to use std::reference_wrapper instead: https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper

From what I can tell from the documentation, this is what one of its intended uses is, but when the topic of arrays containing polymorphic objects comes up, the common advice seems to be to use smart pointers rather than std::reference_wrapper.

My only thought is that smart pointers may be able to handle the lifetime of the object a little neater?

TL:DR; Why are smart pointers, such as std::unique_ptr seemingly preferred over std::reference_wrapper when creating arrays of polymorphic objects?

6
  • 8
    If the objects are owned somewhere else, reference_wrapper is fine. However, if this container owns the objects and controls their lifetime, you must use a smart pointer such as unique_ptr - reference_wrapper does not provide any ownership semantics. Commented Sep 7, 2018 at 12:34
  • 4
    unique_ptr manages the lifetime of the owned object, reference_wrapper stores a pointer to the object. Use unique_ptrs to guarantee the release of the object! Commented Sep 7, 2018 at 12:34
  • 4
    a reference is just a reference, someone still needs to take care of the lifetime of the objects. With a smartpointer you get all you need at once Commented Sep 7, 2018 at 12:34
  • 2
    actually you already have the answer in your question. The only thing to add is that it is not about "neater", but references dont handle the lifetime at all Commented Sep 7, 2018 at 12:35
  • 1
    std::reference_wrapper as a container element type doesn't offer any advantages over a regular dumb pointer, except that the reference wrapper cannot be null. Commented Sep 7, 2018 at 12:50

4 Answers 4

14

In very simple terms:

  • unique_ptr is the owner of the object. It manages the lifetime of the owned object

  • reference_wrapper wraps a pointer to an object in memory. It does NOT manage the lifetime of the wrapped object

You should create an array of unique_ptr (or shared_ptr) to guarantee the release of the object when it's not needed anymore.

Sign up to request clarification or add additional context in comments.

1 Comment

Try to always use smart pointers when you deal with objects that live in the heap. I use pointers like you do when I need to deal with polymorphism but I always try to not use pointers at all!
6

If you are sufficiently motiviated, you can write a poly_any<Base> type.

A poly_any<Base> is an any restricted to only storing objects that derive from Base, and provides a .base() method that returns a Base& to the underlying object.

A very incomplete sketch:

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;
};

Then you just have to intercept every means of putting stuff into the poly_any<Base> and store a get_base function pointer:

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

Once you have done this, you can create a std::vector<poly_any<Base>> and it is a vector of value types that are polymorphically descended from Base.

Note that std::any usually uses the small buffer optimization to store small objects internally, and larger objects on the heap. But that is an implementation detail.

Comments

1

Basically, a reference_wrapper is a mutable reference: Like a reference, it must not be null; but like a pointer, you can assign to it during its lifetime to point to another object.

However, like both pointers and references, reference_wrapper does not manage the lifetime of the object. That's what we use vector<uniq_ptr<>> and vector<shared_ptr<>> for: To ensure that the referenced objects are properly disposed off.

From a performance perspective, vector<reference_wrapper<T>> should be just as fast and memory efficient as vector<T*>. But both of these pointers/references may become dangling as they are not managing object lifetime.

Comments

1

Let's try the experiment:

#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;
      }
   }
}

running with argument shared:

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

running with argument unique

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

running with argument ref

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

Explanation:

  • shared: Memory is shared by different parts of the code. In the example, the Derived object is first owned by the d local var in the function fill_shared() and by the vector. When the code exits the scope of the function object is still owned by the vector and only when the vector goes finally away, the object is deleted
  • unique: Memory is owned by the unique_ptr. In the example, the Derived object is first owned by the d local var. However it must be moved into the vector, transferring the ownership. Same as before, when the only owner goes away, the object gets deleted.
  • ref: There's no owning semantics. The object is created as a local variable of the fill_ref() function, and the reference to the object can be added to the vector. However, the vector does not own the memory and when the code goes out of the fill_ref() function, the object goes away, leaving the vector pointing to unallocated memory.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.