有效地使用std::move来聚合std容器中的所有对象实例



我需要集中地将程序中的某些实体创建累积到一个容器中,并且我希望使用std::move和move构造函数的效率来将程序中任何地方创建的所有实体聚合到一个容器中,而不需要额外的复制或实例分配。不幸的是,使用最流行的std::vector容器会带来vector内部管理开销(或者依赖于编译器实现吗??)

例如

class Item {
public :
static int Count;
int ID;
Item() : ID(Count++)
{ cout<<"   Item CREATED - ID:"<<ID<<endl; }
Item(const Item &itm) : ID(Count++)
{ cout<<"   Item COPY CREATED - (ID:"<<ID<<") <= (ID:"<<itm.ID<<")n"; }
Item(const Item &&itm) : ID(Count++)
{ cout<<"   Item MOVE CREATED - (ID:"<<ID<<") <= (ID:"<<itm.ID<<")n"; }
~Item() { cout<<" Item DELETED - (ID:"<<ID<<") nn"; }
};
int Item::Count = 0;
void VectorOfItemTest() {
std::vector<Item> ItemVec;
for(int idx=0; idx<3; idx++) {
std::cout<<" { loop "<<idx<<std::endl;
Item itemInst;
ItemVec.push_back(std::move(itemInst));
std::cout<<" } "<<idx<<std::endl;
}
}

产生输出:

-----------------------------
{ loop 0
Item CREATED - ID:0
Item MOVE CREATED - (ID:1) <= (ID:0)
} 0
Item DELETED - (ID:0)
{ loop 1
Item CREATED - ID:2
Item MOVE CREATED - (ID:3) <= (ID:2)
Item COPY CREATED - (ID:4) <= (ID:1)
Item DELETED - (ID:1)
} 1
Item DELETED - (ID:2)
{ loop 2
Item CREATED - ID:5
Item MOVE CREATED - (ID:6) <= (ID:5)
Item COPY CREATED - (ID:7) <= (ID:4)
Item COPY CREATED - (ID:8) <= (ID:3)
Item DELETED - (ID:4)
Item DELETED - (ID:3)
} 2
Item DELETED - (ID:5)
Item DELETED - (ID:7)
Item DELETED - (ID:8)
Item DELETED - (ID:6)

是否有可能避免在for循环中导致匹配delete-s的额外copy-s ?

是否存在一个容器(或者我们可以以任何方式使用std::vector),使所有循环输出看起来如下?

{ loop X
Item CREATED - ID:X
Item MOVE CREATED - (ID:X+1) <= (ID:X)
} X
Item DELETED - (ID:X)

我看过为什么std::move需要调用std::vector的移动赋值操作符为什么std::move复制右值或const左值函数参数的内容?和其他一些在这里,但它仍然不清楚我如何使用std::vector(或其他容器)有效地使用std::move.

我发现了一个被拒绝的问题很难理解对象的生存期,复制,移动构造函数问接近我在这里指的我猜。

[UPDATE 1]使用指针:我现有的代码使用指针避免了额外的分配和复制。我试图消除指针的使用通过我的代码向前移动-因此这个问题。如果这个更改使内存分配和复制次数加倍,我将恢复使用指针。

[UPDATE 2] @MooingDuck建议使用std::deque解决了这个问题,而不需要reserve()或noexcept移动构造函数。我实际上是在设计std::vector包装器数组的过程中,以避免std::vector调整大小,因为我还需要指向实体的指针保持有效,以支持遗留代码。std::deque似乎就是这样做的

"The storage of a deque is automatically expanded and contracted
as needed. Expansion of a deque is cheaper than the expansion of
a std::vector because it does not involve copying of the existing
elements to a new memory location. On the other hand, deques 
typically have large minimal memory cost; a deque holding just one 
element has to allocate its full internal array (e.g. 8 times the 
object size on 64-bit libstdc++; 16 times the object size or 4096 
bytes, whichever is larger, on 64-bit libc++)."

deque test

void DequeOfItemTest() {
std::deque<Item> ItemDQ;
for(int idx=0; idx<3; idx++) {
std::cout<<" { deque loop "<<idx<<std::endl;
Item itemInst;
ItemDQ.push_back(std::move(itemInst));
Item &refToItem = ItemDQ[ItemDQ.size()-1];
Item *ptrToItem = &refToItem;
std::cout<<" } "<<idx<<std::endl;
}
}

现在产生了与我想要的相同的输出-堆栈上的单个实体分配,然后单个移动到容器中,然后单个删除堆栈分配

-----------------------------
{ deque loop 0
Item CREATED - ID:0
Item MOVE CREATED - (ID:1) <= (ID:0)
} 0
Item DELETED - (ID:0)
{ deque loop 1
Item CREATED - ID:2
Item MOVE CREATED - (ID:3) <= (ID:2)
} 1
Item DELETED - (ID:2)
{ deque loop 2
Item CREATED - ID:4
Item MOVE CREATED - (ID:5) <= (ID:4)
} 2
Item DELETED - (ID:4)
Item DELETED - (ID:5)
Item DELETED - (ID:3)
Item DELETED - (ID:1)
-----------------------------

[注]正如下面的评论和回答(以及VS2022)所建议的那样,声明move构造函数和move赋值操作符为noexcept是一种良好的实践,因为它允许像std::vector这样的容器使用更有效的move操作而不是复制操作。

要达到你所陈述的理想,需要做两件事:

首先,您必须将移动构造函数标记为noexcept。如果它抛出异常,则调用std::terminate,因此必须将其设计为永远不会抛出异常。

Item(const Item &&itm) noexcept : ID(Count++)
{ cout<<"   Item MOVE CREATED - (ID:"<<ID<<") <= (ID:"<<itm.ID<<")n"; }

下面是:

{ loop 0
Item CREATED - ID:0
Item MOVE CREATED - (ID:1) <= (ID:0)
} 0
Item DELETED - (ID:0) 
{ loop 1
Item CREATED - ID:2
Item MOVE CREATED - (ID:3) <= (ID:2)
Item MOVE CREATED - (ID:4) <= (ID:1)
Item DELETED - (ID:1) 
} 1
Item DELETED - (ID:2) 
{ loop 2
Item CREATED - ID:5
Item MOVE CREATED - (ID:6) <= (ID:5)
Item MOVE CREATED - (ID:7) <= (ID:3)
Item MOVE CREATED - (ID:8) <= (ID:4)
Item DELETED - (ID:3) 
Item DELETED - (ID:4) 
} 2
Item DELETED - (ID:5) 
Item DELETED - (ID:6) 
Item DELETED - (ID:7) 
Item DELETED - (ID:8) 

上面唯一改变的是你之前的拷贝变成了招式。

这样做的原因是为了使vector::push_back能够保持c++ 98/03的强异常保证。这意味着如果在push_back期间抛出任何异常,则vector的值不会发生变化。

第二,你需要reserve在向量中有足够的空间,这样push_back就不需要分配更大的缓冲区:

std::vector<Item> ItemVec;
ItemVec.reserve(3);

这使你达到理想状态:

{ loop 0
Item CREATED - ID:0
Item MOVE CREATED - (ID:1) <= (ID:0)
} 0
Item DELETED - (ID:0) 
{ loop 1
Item CREATED - ID:2
Item MOVE CREATED - (ID:3) <= (ID:2)
} 1
Item DELETED - (ID:2) 
{ loop 2
Item CREATED - ID:4
Item MOVE CREATED - (ID:5) <= (ID:4)
} 2
Item DELETED - (ID:4) 
Item DELETED - (ID:5) 
Item DELETED - (ID:3) 
Item DELETED - (ID:1) 

push_backItemVec.size() == ItemVec.capacity()调用时,一个新的缓冲区被分配,所有现有的元素都被移动到新的缓冲区…除非您的move构造函数不是noexcept,在这种情况下,所有现有元素都被复制到新的缓冲区

相关内容

最新更新