提问人:sith 提问时间:12/28/2022 最后编辑:sith 更新时间:12/29/2022 访问量:87
高效使用 std::move 聚合 std 容器中的所有对象实例
efficient use of std::move to aggregate all object instances in a std container
问:
我需要将程序中的某些实体创建集中到一个容器中,并且我想将 std::move 的效率与移动构造函数一起使用,以将程序中创建的所有实体聚合到一个容器中,而无需额外的复制或实例分配。不幸的是,使用最流行的 std::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<<") \n\n"; }
};
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 会复制 rvalue 或 const 左值函数参数的内容? 还有一些其他的,但仍然不清楚我如何使用 std::move 有效地使用 std::vector(或其他容器)。
我发现了一个被拒绝的问题,很难理解对象生命周期、复制、移动构造函数,我猜接近我在这里指的是什么。
[更新1]使用指针:我现有的代码使用指针,避免了额外的分配和复制。我正试图在我的代码中消除指针的使用——因此这个问题。如果此更改使内存分配和 copy-s 加倍,我将恢复到指针。
[更新 2] @MooingDuck使用 std::d eque 的建议解决了这个问题,而无需 reserve() 或 noexcept 移动构造函数。我实际上正在设计一个 std::vector 包装器数组以避免 std::vector 调整大小,因为我还需要指向实体的指针保持有效以支持遗留代码。std::d eque 似乎正是这样做的
"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测试
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)中所建议的那样,将移动构造函数和移动赋值运算符声明为 noexcept 是一种很好的做法,因为它允许像 std::vector 这样的容器使用更有效的移动操作而不是复制操作。
答:
要实现您所说的理想,需要两件事:
首先,您必须将移动构造函数标记为 。如果它随后抛出异常,则被调用,因此它确实必须被设计为永远不会抛出。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)
上面唯一改变的是,你之前的副本变成了移动。
之所以需要这样做,是为了维护 C++98/03 的强异常保证。这意味着,如果在 期间抛出任何异常,则 的值不会发生任何变化。vector::push_back
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_back
ItemVec.size() == ItemVec.capacity()
noexcept
评论
emplace_back
emplace_back
没有为我更改日志记录输出(完成前两个步骤后)。
emplace_back
评论
std::deque
noexcept
ItemDQ.emplace_back(...)
noexcept
std::vector
noexcept