使用 `std::vector` 存储异构的 unique_ptrs

huangapple go评论67阅读模式
英文:

Using std::vector<std::any> to store heterogenous unique_ptrs

问题

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&&'}

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.

英文:

I want to store differently typed unique_ptrs in a vector.

I attempted using std::any as follows.

#include &lt;vector&gt;
#include &lt;any&gt;
#include &lt;memory&gt;
class A
{
int val;
};
class B
{
float val;
};
int main()
{
std::vector&lt;std::any&gt; vec;
auto a = new A();
auto b = new B();
vec.push_back(std::unique_ptr&lt;A&gt;(a));
vec.push_back(std::unique_ptr&lt;B&gt;(b));
}

It is failing as follows.

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

答案1

得分: 8

std::unique_ptr 不能用作 std::any,因为后者要求值类型是可复制构造的,而 std::unique_ptr 不是。


根据您描述的用例:

一个直接的解决方案是改用 std::shared_ptr,因为它是可复制构造的。

然而,在这种情况下 std::any 完全不是必需的。所有 std::shared_ptr 实例都可以随时转换为 std::shared_ptr&lt;void&gt;。删除器是类型擦除的,并且仍将按预期调用:

std::vector&lt;std::shared_ptr&lt;void&gt;&gt; vec;
auto new_a = std::make_shared&lt;A&gt;();
A* a = new_a.get();
vec.push_back(std::move(new_a));
auto new_b = std::make_shared&lt;B&gt;();
B* b = new_b.get();
vec.push_back(std::move(new_b));
// 在此处使用 a 和 b,假设 vec 的生存周期超出它们

元素需要直接构造为 shared_ptr(例如,使用 std::make_shared),因为否则在 new 表达式和 shared_ptr 构造之间发生异常将导致内存泄漏。std::move 是可选的。

然而,std::shared_ptr 比你实际需要的要多得多。std::unique_ptr 没有一个可以使用的 void 实例,因为删除器的类型没有被擦除,但您可以通过编写一个从具有虚拟析构函数的单个非模板基类派生的 std::unique_ptr 等效物来实现相同的效果。然后,您可以在您的向量中使用该基类作为元素类型。

(如果您对额外的间接性感到满意,可以通过在派生模板中直接使用 std::unique_ptr,然后在基类中使用 std::unique_ptr&lt;base&gt; 来轻松实现这一点。如果您不希望使用间接性,那么我认为您将不得不从头开始实现它。我想不出一个有用的标准库功能。)


还可以实现自己的支持不可复制类型的 std::any 等效物。std::any 简单地决定支持复制,一旦做出这个决定,所有可能包含的类型必须支持它。但这比我上面建议的更复杂。


然而,更简单的解决方案是,假设对于您而言这样的设计和性能是可接受的,那就是所有您打算存储的类 AB 等都派生自带有虚拟析构函数的 Base,在这种情况下,简单地 std::vector&lt;std::unique_ptr&lt;Base&gt;&gt; 也可以。在这种情况下,虚拟析构函数是必需的!否则,您将会有未定义行为!

英文:

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&lt;void&gt;. The deleter is type-erased and will still be called as expected:

std::vector&lt;std::shared_ptr&lt;void&gt;&gt; vec;
auto new_a = std::make_shared&lt;A&gt;();
A* a = new_a.get();
vec.push_back(std::move(new_a));
auto new_b = std::make_shared&lt;B&gt;();
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&lt;base&gt; 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&lt;std::unique_ptr&lt;Base&gt;&gt; will also do it. In this case the virtual destructor is however required! Otherwise you will have undefined behavior!

答案2

得分: 3

如果您只需要销毁对象,那么可以做一些更简单的事情,例如像这样:

#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);
}
英文:

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

#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;functional&gt;
#include &lt;utility&gt;

class A
{
public:
    ~A() {std::cout &lt;&lt; &quot;~A()\n&quot;;}
   int val;
};

class B
{
public:
    ~B() {std::cout &lt;&lt; &quot;~B()\n&quot;;}
   float val;
};

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

int main()
{
    ObjKeeper keep;
    
    auto a = new A();
    auto b = new B();
    
    keep.addObj(a);
    keep.addObj(b);
}

答案3

得分: 1

以下是翻译好的部分:

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

std::vector&lt;std::any&gt; 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."

英文:

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

std::vector&lt;std::any&gt; 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.

huangapple
  • 本文由 发表于 2023年2月18日 07:49:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/75490213.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定