C++17 In Detail

09 March 2020

Const collection of unique_ptr, options and design choices

Today I have one use case for you: how to declare a const collection of unique pointers of polymorphic types? Can we use std::vector? or maybe std::array? What are the options here? What if we’d like to have such collection as a class member? Have a look in the article.

Use Case

Here’s the code idea:

struct Base { virtual ~Base() = default; virtual void doStuff() = 0; };
struct A : Base { void doStuff() override { } };
struct B : Base { void doStuff() override { } };
struct C : Base { void doStuff() override { } };

As you can see, we have a bunch of derived classes that implement doStuff() virtual function. We’d like to build a collection of pointers to Base, so that we can call the function polymorphically. To be precise: we want a const collection, but objects inside won’t be const. In other words, I don’t want to add/remove things from the collection, but I want to call non-const member functions on the objects - something like a pool of objects to reuse.

Originally I had this use case for my algorithm visualisation application, where I needed a collection of pointers to Algorithm classes (see another post “Use the Force, Luke”… or Modern C++ Tools). Each class represents a different sorting algorithm, and throughout the lifetime users could switch from one algorithm to another. The number of algorithms is fixed at the start of the program.

We can of course do this:

Base* const collectionRaw[] = { new A(), new B(), new C()};
collectionRaw[0]->doStuff(); // test call

// ...
// remember to "delete" each array member of collectionRaw!

But let’s not go that way, as it’s definitely not modern C++.

What are the options then?

unique_ptr to save the world?

How about unique_ptr? This type of smart pointer allows us to use RAII and wraps the raw pointer. Also it will destroy it for us. If you’re not convinced, you can also read my older article called: 5 ways how unique_ptr enhances resource safety in your code.

We can easily change the code into:

const std::unique_ptr<Base> rawArr[] = {
        std::make_unique<A>(),
        std::make_unique<B>(),
        std::make_unique<C>()
    };

rawArr[0]->doStuff();

That’s great, and we have a simple const array.

What if we complicate things a bit?

As A class member?

In my first use case, I need to have such an array as a class member. The class represents the “Application State”.

Shouldn’t be a big deal, let’s write:

struct MyClass {    
    const std::unique_ptr<Base> m_rawArr[] = {
        std::make_unique<A>(),
        std::make_unique<B>(),
        std::make_unique<C>()
    };
};

Unfortunately in GCC it gives me:

main.cpp:13:33: warning: ISO C++ forbids flexible array member 'm_rawArr' [-Wpedantic]

To avoid that we have to provide the size of the array:

struct MyClass {    
    const std::unique_ptr<Base> m_rawArr[3] = {
        std::make_unique<A>(),
        std::make_unique<B>(),
        std::make_unique<C>()
    };
};

Now it compiles.

Hmm… but cannot the compiler extract the information about the size of my array. Shouldn’t that be simple?

I’d really like to avoid the need to provide a number of elements. In the final application, you might add or remove items from the collection (at compile time), so you’d always have to remember about changing the parameter. In our code it’s [3]…, and since it’s a magic number, we’d probably want to extract that to some constant value.

How about std::array? It has CTAD since C++17 so that it could deduce all the template types…

Unfortunately, we cannot use std::array either, as in our initialiser list I use different types, so the compiler won’t deduce the proper type… plus CTAD is not allowed for non-static data member initialisation.

That’s why we have the same problem as with a regular array, where we have to provide the number of elements:

struct MyClassArr {
    const std::array<std::unique_ptr<Base>, 3> m_arr = {
        std::make_unique<A>(),
        std::make_unique<B>(),
        std::make_unique<C>()
    };
};

Using a standard container?

How about std::vector?

std::vector will allocate extra memory on the heap, but if that’s not a big issue for us. Let’s try that:

struct MyClassVec {
    const std::vector<std::unique_ptr<Base>> m_vec = {
        std::make_unique<A>(),
        std::make_unique<B>(),
        std::make_unique<C>()
    };
};

Super cool! We don’t need to specify the number of elements… but will that compile?

Eh…

You can try to read the full message…

TLDR: we cannot use the initializer list to init a vector with moveable only types.

How to solve the issue?

We have two options here:

  • use shared_ptr which is copyable
  • think about some extra code that would perform the initialisation

Using shred_ptr might be the most comfortable option, but here we need to pay the price of additional reference counting mechanism, plus it also changes the meaning of our class. I’m not in favour of such an approach, so let’s stick with unique_ptr.

We have to implement a method that would solve the issue with moveable only types.

So let’s try to come up with some handy function that would create such a vector for us:

template<typename T, typename... Args>
auto initFromMoveable(Args&&... args)
{
    std::vector<std::unique_ptr<T>> vec;
    vec.reserve(sizeof...(Args)); 
    (vec.emplace_back(std::forward<Args>(args)), ...);
    return vec;
}

struct MyClassVecFunc {
    const std::vector<std::unique_ptr<Base>> m_vec = initFromMoveable<Base>(
        std::make_unique<A>(),
        std::make_unique<B>(),
        std::make_unique<C>()
    );
};

That’s nice!

We pay the price of vector creation and memory allocation, but we don’t have to specify the number of objects!

Note that I used a few modern C++ things here:

  • variadic templates - C++11
  • emplace_back method from std::vector that was added in C++11
  • auto return type deduction - C++14
  • make_unique from C++14
  • fold expression(over comma) - C++17
  • copy elision (common across many compilers)

One more use case: how about a constant map?

template<typename Key, typename T, typename... Args>
auto initMapFromMoveable(Args&&... args)
{
    map<Key, unique_ptr<T>> map;
    (map.emplace(forward<Args>(args)), ...);
    return map;
}

struct MyClassMapFunc {
    const map<int, unique_ptr<Base>> m_map = initMapFromMoveable<int, Base>(
        pair{ 10, make_unique<A>() },
        pair{ 11, make_unique<B>() },
        pair{ 12, make_unique<C>() }
    );
};

A completely different approach: using value type and std::variant

The main reason for using pointers in the collection was to be able to call DoStuff() polymorphically.

However, since C++17, we have another way: using std::variant and std::visit to perform polymorphism. What’s more, rather than with pointers, we can now work with regular value types.

Here’s a basic example for our use case:

using ElementType = std::variant<A, B, C>
const std::vector<ElementType> collection { A{}, B{}, C{}};

auto DoStuffCaller = [](auto& obj) { return obj.DoStuff(); };
std::visit(DoStuffCaller, collection[id]);

As you can see in the example, I used std::variant to build the collection and then call std::visit with a callable object that then invokes doStuff().

But… can you spot a semantic problem here?

.

.

.

The whole collection is now const, so the elements inside are also const. And I cannot call non-const methods there.

While the whole idea is promising, it doesn’t work for my use case. Unless maybe, I create something like a custom const collection that blocks adding/removing elements but allows changing them.

And you can read about std::visit and std::variant in my separate blog posts, for example: Everything You Need to Know About std::variant from C++17.

Other Ideas - do I really need all the pointers?

When I started messing around with those arrays, vectors, pointers, I noticed that I might miss an important design decision: Do I need all of those objects to be alive all the time?

If the creation of an object costs a lot, then it’s probably fine to have a “pool” of such objects. However, in my case, my classes were simple things. It was easy to create and destroy them on demand. And such event occurs maybe several times per minute (when a user switches between menu entries).

Another reason for having several objects might be when you need all of them to be active at the same time. For example, if you have a collection of game actors, you need to Update() them and maybe Render() later. In my case, it was only one algorithm visualisation class that is active at a given time.

So… why not throw away all of that complicated stuff with arrays, moveable only types… and just have:

std::unique_ptr<Base> m_currentObject;

Summary

To sum up:

If you want to init a const collection of unique_ptr on the stack:

  • const std::unique_ptr<Base> arr[] = { }; - works fine
  • const std::array<> - might be ok, but you need to specify the template parameters as the compiler cannot deduce the pointer to the base class from the derived objects
  • using std::vector might also be fine, but you’ll pay extra cost for a memory allocation

But if you want to do that as a class member.

  • array works, but you need to provide the number of elements
  • std::array - same issue
  • std::vector - you cannot use initialiser list as unique_ptr is moveable only, so some extra code is needed, and also you need to pay the price of extra mem allocation

Extra: you can also have a look at std::variant which enables to use of polymorphic classes but with value types.

But, it’s also essential to think about the design of the whole problem. In my case, I didn’t need a collection, so having only a single pointer was good enough.

Source code:

http://coliru.stacked-crooked.com/a/9617f4f94e90257c
with a map: http://coliru.stacked-crooked.com/a/a498115cdd3075aa

C++17 In Detail
© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.