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?
In very simple terms:
You should create an array of unique_ptr
(or shared_ptr
) to guarantee the release of the object when it's not needed anymore.
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.
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.
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.