如何在std::vector中存储不带复制或移动构造函数的对象

How to store objects without copy or move constructor in std::vector?

本文关键字:移动 复制 构造函数 对象 存储 std vector      更新时间:2023-10-16

为了提高std::vector<T>的效率,它的底层数组需要预先分配,有时还需要重新分配。然而,这需要创建T类型的对象,然后使用复制ctor或移动ctor进行移动。

我遇到的问题是T无法复制或移动,因为它包含无法复制或删除的对象(如atomicmutex)。(是的,我正在实现一个简单的线程池。)

我想避免使用指针,因为:

  1. 我不需要间接手段,所以我不想要间接手段
  2. (指针效率较低,增加了复杂性。使用指针会增加内存碎片并降低数据局部性,这可能(但不一定必须)会对性能产生显著影响。不那么重要,但仍然值得考虑。)

这里有没有一种方法可以避免一定程度的间接性?

更新:我修正了一些错误的假设,并根据评论和答案中的反馈重新措辞了这个问题

首先,std::mutex无法复制或移动,因此您必须使用某种间接方式。

由于您希望将互斥对象存储在向量中,而不是复制它,所以我将使用std::unique_ptr

vector<unique_ptr<T>>不允许某些矢量操作(例如for_each)

我不确定我是否理解那句话。完全有可能为进行测距

std::vector< std::unique_ptr< int > > v;
// fill in the vector
for ( auto & it : v )
  std::cout << *it << std::endl;

或者使用std算法:

#include <iostream>
#include <typeinfo>
#include <vector>
#include <memory>
#include <algorithm>

int main()
{
    std::vector< std::unique_ptr< int > > v;
    v.emplace_back( new int( 3 ) );
    v.emplace_back( new int( 5 ) );
    std::for_each( v.begin(), v.end(), []( const std::unique_ptr< int > & it ){ std::cout << *it << std::endl; } );
}

然而,这需要使用复制ctor创建T类型的对象。

这并不完全正确,从C++11开始,如果您使用std::vector的构造函数,它将默认构造许多元素,那么您就不需要复制或移动构造函数。

因此,如果没有线程从池中添加或删除,那么您可以执行以下操作:

int num = 23;
std::vector<std::mutex> vec(num);

如果你想动态地添加或删除东西,那么你必须使用间接方法。

  1. 使用已建议的std::vector+std::unique_ptr
  2. 使用std::deque,这使您可以巧妙地将其与基于范围的for循环或std算法一起使用,并避免所有间接寻址。(只允许添加)
  3. 使用std::list/forward_list此解决方案与第一个解决方案类似,但它具有更容易使用基于范围的for和算法的额外好处。如果只按顺序访问元素可能是最好的,因为不支持随机访问

像这样:

std::deque<std::mutex> deq;
deq.emplace_back();
deq.emplace_back();
for(auto& m : deq) {
    m.lock();
}

最后要注意的是,std::thread当然是可移动的,因此您可以将std::vector+std::vector::emplace_back与之一起使用。

总结到目前为止提出的内容:

  1. 使用vector<unique_ptr<T>>--添加一个显式间接级别,这是OP不需要的
  2. 使用deque<T>——我第一次尝试deque,但从中擦除对象也不起作用。参见本讨论dequelist之间的差异

解决方案是使用forward_list,它是一个单链表(如果您想要一个双链表,也可以使用list)。正如@JanHudec所指出的,vector(以及它的许多朋友)在添加或删除项目时需要重新分配。这不适合像mutexatomic这样不允许复制或移动的对象。forward_listlist不需要这样做,因为每个小区都是独立分配的(我不能引用这方面的标准,但索引方法产生了这种假设)。由于它们实际上是链表,因此不支持随机访问索引。myList.begin() + i将为您获得第i个元素的迭代器,但它(最肯定的)必须首先遍历所有以前的i单元格。

我没有按照标准查看这些承诺,但在Windows(Visual Studio)和CompileOnline(g++)上运行良好。请随意在CompileOnline上玩以下测试用例:

#include <forward_list>
#include <iostream>
#include <mutex>
#include <algorithm>
using namespace std;
class X
{
    /// Private copy constructor.
    X(const X&);
    /// Private assignment operator.
    X& operator=(const X&);
public:
    /// Some integer value
    int val;
    /// An object that can be neither copied nor moved
    mutex m;
    X(int val) : val(val) { }
};

int main()
{
    // create list
    forward_list<X> xList;
    // add some items to list
    for (int i = 0; i < 4; ++i)
       xList.emplace_front(i);
    // print items
    for (auto& x : xList)
       cout << x.val << endl;
    // remove element with val == 1    
    // WARNING: Must not use remove here (see explanation below)
    xList.remove_if([](const X& x) { return x.val == 1; });

    cout << endl << "Removed '1'..." << endl << endl;
    for (auto& x : xList)
       cout << x.val << endl;
   return 0;
}

输出:

Executing the program....
$demo 
3
2
1
0
Removed '1'...
3
2
0

我预计这将具有与vector<unique_ptr<T>>大致相同的性能(只要您不经常使用随机访问索引)。

警告:使用forward_list::remove目前在VS 2012中不起作用。这是因为它在试图删除元素之前复制了它。头文件Microsoft Visual Studio 11.0VCincludeforward_list(与list中的问题相同)显示:

void remove(const _Ty& _Val_arg)
{   // erase each element matching _Val
    const _Ty _Val = _Val_arg;  // in case it's removed along the way
    // ...
}

因此,它会被复制,"以防一路上被删除"。这意味着listforward_list甚至不允许存储unique_ptr。我认为这是一个设计错误。

解决方法很简单:必须使用remove_if而不是remove,因为该函数的实现不会复制任何内容。

很多功劳都归于其他答案。然而,由于没有一个是没有指针的完整解决方案,我决定写下这个答案

可以在std::vector中存储不带移动或复制构造函数的元素,但您只需避免要求元素具有移动或复制构造器的方法。几乎1任何改变矢量大小的东西(例如,push_backresize()等)。

在实践中,这意味着您需要在构建时分配一个固定大小的向量,它将调用对象的默认构造函数,您可以通过赋值对其进行修改。这至少适用于std::atomic<>对象,可以将其分配给.


1clear()是不需要复制/移动构造函数的大小更改方法的唯一示例,因为它从不需要移动或复制任何元素(毕竟,此操作后向量为空)。当然,在调用此命令后,您再也不能增长零大小的向量了!