5

I want to store differently typed unique_ptrs in a vector.

I attempted using std::any as follows.

#include <vector>
#include <any>
#include <memory>

class A
{
   int val;
};

class B
{
   float val;
};


int main()
{
    std::vector<std::any> vec;
    
    auto a = new A();
    auto b = new B();
    
    vec.push_back(std::unique_ptr<A>(a));
    vec.push_back(std::unique_ptr<B>(b));
}

It is failing as follows.

main.cpp: In function 'int main()':
main.cpp:23:18: error: no matching function for call to 'std::vector<std::any>::push_back(std::unique_ptr<A>)'
   23 |     vec.push_back(std::unique_ptr<A>(a));
      |     ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/local/include/c++/12.1.0/vector:64,
                 from main.cpp:1:
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1276:7: note: candidate: 'void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::any; _Alloc = std::allocator<std::any>; value_type = std::any]'
 1276 |       push_back(const value_type& __x)
      |       ^~~~~~~~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1276:35: note:   no known conversion for argument 1 from 'std::unique_ptr<A>' to 'const std::vector<std::any>::value_type&' {aka 'const std::any&'}
 1276 |       push_back(const value_type& __x)
      |                 ~~~~~~~~~~~~~~~~~~^~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1293:7: note: candidate: 'void std::vector<_Tp, _Alloc>::push_back(value_type&&) [with _Tp = std::any; _Alloc = std::allocator<std::any>; value_type = std::any]'
 1293 |       push_back(value_type&& __x)
      |       ^~~~~~~~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1293:30: note:   no known conversion for argument 1 from 'std::unique_ptr<A>' to 'std::vector<std::any>::value_type&&' {aka 'std::any&&'}
 1293 |       push_back(value_type&& __x)
      |                 ~~~~~~~~~~~~~^~~
main.cpp:24:18: error: no matching function for call to 'std::vector<std::any>::push_back(std::unique_ptr<B>)'
   24 |     vec.push_back(std::unique_ptr<B>(b));
      |     ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1276:7: note: candidate: 'void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::any; _Alloc = std::allocator<std::any>; value_type = std::any]'
 1276 |       push_back(const value_type& __x)
      |       ^~~~~~~~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1276:35: note:   no known conversion for argument 1 from 'std::unique_ptr<B>' to 'const std::vector<std::any>::value_type&' {aka 'const std::any&'}
 1276 |       push_back(const value_type& __x)
      |                 ~~~~~~~~~~~~~~~~~~^~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1293:7: note: candidate: 'void std::vector<_Tp, _Alloc>::push_back(value_type&&) [with _Tp = std::any; _Alloc = std::allocator<std::any>; value_type = std::any]'
 1293 |       push_back(value_type&& __x)
      |       ^~~~~~~~~
/usr/local/include/c++/12.1.0/bits/stl_vector.h:1293:30: note:   no known conversion for argument 1 from 'std::unique_ptr<B>' to 'std::vector<std::any>::value_type&&' {aka 'std::any&&'}
 1293 |       push_back(value_type&& __x)
      |              

Is this possible with std::any? Or is there any alternative? I am unable to use std::variant as I don't know all the types to be stored upfront.

Edit:

I just want to use the unique_ptr vector to enforce that objects will be cleaned up at program exit (the vector will be alive till program exit). The consumer code will use direct references to a and b so I don't need to access/enumerate object references via the vector.

catluna
  • 65
  • 4
  • How do you intent to use the elements in the vector if you don't know anything about their type? – user17732522 Feb 17 '23 at 23:50
  • 2
    `std::any` requires the type to be copy-constructible, which `unique_ptr` is not. But even if it was possible, I don't see how it would be useful, see question above. – user17732522 Feb 17 '23 at 23:52
  • I just want to use the unique_ptr vector to enforce that objects will be cleaned up at program exit (the vector will be alive till program exit). The consumer code will use direct references to `a` and `b` so I don't need to access/enumerate object references via the vector. – catluna Feb 17 '23 at 23:57
  • 1
    Please add that to the question. `std::unique_ptr` won't work for that, so you should be asking how to reach that goal instead. – user17732522 Feb 18 '23 at 00:00

3 Answers3

8

std::unique_ptr cannot be used as a std::any, because the latter requires the value type to be copy-constructible, which std::unique_ptr is not.


Given the use case that you described:

A straight-forward solution would be to use std::shared_ptr instead, which is copy-constructible.

However, in that case std::any is not necessary at all. All std::shared_ptr instances can always be converted to std::shared_ptr<void>. The deleter is type-erased and will still be called as expected:

std::vector<std::shared_ptr<void>> vec;

auto new_a = std::make_shared<A>();
A* a = new_a.get();
vec.push_back(std::move(new_a));

auto new_b = std::make_shared<B>();
B* b = new_b.get();
vec.push_back(std::move(new_b));

// use a and b here, assuming that vec outlives them

The element needs to be constructed directly into a shared_ptr (e.g. with std::make_shared), because otherwise an exception inbetween the new expression and construction of the shared_ptr will cause a memory leak. std::move is optional.

However, std::shared_ptr is much more than you really need. std::unique_ptr doesn't have a void instance that you can use because the deleter's type is not erased, but you can achieve the same effect by writing a std::unique_ptr equivalent which derives from a single non-template base class with virtual destructor. Then you can use that base class as element type in your vector.

(If you are confortable with an additonal indirection, this can be easily implemented by using std::unique_ptr itself in the derived template and then using std::unique_ptr<base> in the base class. If you don't want the indirection, then I think you'll have to implement it from scratch. I can't think of a helpful standard library functionality.)


It is also possible to implement your own std::any equivalent that supports non-copyable types. std::any simply made the decision to support copying and once that decision is made all potentially contained types must support it. But that's more complex than what I suggested above.


However, an even easier solution, assuming that is acceptable design and performance for you, is to have all classes A, B, etc. that you intent to store derive from some Base with virtual destructor, in which case simply std::vector<std::unique_ptr<Base>> will also do it. In this case the virtual destructor is however required! Otherwise you will have undefined behavior!

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • What a nice answer. You could add, that a custom any implementation could also fix the issue. One that could move a value into the holder for example, or a perfectly forwarding any. – user1095108 Feb 18 '23 at 00:29
  • 1
    @user1095108 What I am suggesting at the end is basically a subset of a custom `std::any` implementation. The actual functionality of `std::any` to be able to access the contained value in a type safe manner is not required for what OP wants to achieve. So only the type-erased destructor is left. – user17732522 Feb 18 '23 at 00:33
3

If you need it only to destroy objects, then you can do something much simpler, for example like this:

#include <iostream>
#include <vector>
#include <functional>
#include <utility>

class A
{
public:
    ~A() {std::cout << "~A()\n";}
   int val;
};

class B
{
public:
    ~B() {std::cout << "~B()\n";}
   float val;
};

class ObjKeeper
{
public:
    ~ObjKeeper() {
        for (auto& f : objs) {
            f();
        }
    }
    template<typename T>
    void addObj(T *obj) {
        objs.push_back([obj]() {delete obj;});
    }
private:
    std::vector<std::function<void()>> objs;
};

int main()
{
    ObjKeeper keep;
    
    auto a = new A();
    auto b = new B();
    
    keep.addObj(a);
    keep.addObj(b);
}
sklott
  • 2,634
  • 6
  • 17
1

In addition to what others wrote, I'd also say that storing a smart pointer instance in

std::vector<std::any> vec;

is an anti-pattern or a code smell.

This is because a std::any instance, similarly to a smart pointer instance, will take over unique ownership over the object it holds, while a smart pointer will only take over the ownership of a pointer. However, if you copy a std::any instance, unlike a std::unique_ptr instance, it will want to copy the object it holds. Therefore, I'd just try doing this first. A std::any instance will destroy the object it holds, so there is no need for a smart pointer in that regard.

Your application design could probably be improved, but writing a custom std::any, that behaves differently than the standard one (you could forbid copying or implement shared ownership, for example), is a worthy project, as type erasure is a recurring topic in c++.

Lastly, you can use memory sanitizers, if you're worried about memory leaks.

user1095108
  • 14,119
  • 9
  • 58
  • 116