I need to centrally accumulate certain entity creations in my program into a container and I want to use the efficiency of std::move with a move constructor to aggregate all entities created anwhere in the program into one container witout extra copying or instance allocations. Unfortunately using the most popular std::vector container brings with it vector internal management overhead ( or is that compiler implementation dependent??)
For example
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"; }
};
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<<std::endl;
}
}
produces output :
-----------------------------
{ 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)
Is it possible to avoid the extra copy-s that causes matching delete-s inside the for loop?
Is there a container (or can we use std::vector in any way) where we get all loop outputs looking as follows ?
{ loop X
Item CREATED - ID:X
Item MOVE CREATED - (ID:X 1) <= (ID:X)
} X
Item DELETED - (ID:X)
I have looked at Why std::move is required to invoke move assign operator of std::vector Why does std::move copy contents for a rvalue or const lvalue function argument? and a few others here but its still not clear how I can use std::vector (or other containers) efficiently using std::move.
I found a rejected question Hard time understanding object lifetime, copy, move constructor asking close to what I am referring to here I guess.
[UPDATE 1] Using pointers : My existing code uses pointers which avoid extra allocation and copy. I am trying to eliminate pointer usage through out my code moving forward - hence this question. I will revert to pointers if this change doubles the memory allocations and copy-s.
CodePudding user response:
There are two things required to get your stated ideal:
First, you'll have to mark your move constructor as noexcept
. And if it then throws an exception, std::terminate
is called, so it really must be designed so that it never throws.
Item(const Item &&itm) noexcept : ID(Count )
{ cout<<" Item MOVE CREATED - (ID:"<<ID<<") <= (ID:"<<itm.ID<<")\n"; }
This gets you down to:
{ 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)
The only thing that has changed above is that your copies from before are turned into moves.
The reason this is needed is so that vector::push_back
can maintain the strong exception guarantee from C 98/03. This means that if any exception is thrown during the push_back
, there are no changes to the value of the vector
.
Second, you need to reserve
sufficient space in the vector so that push_back
never needs to allocate a bigger buffer:
std::vector<Item> ItemVec;
ItemVec.reserve(3);
This gets you down to the ideal:
{ 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)
When push_back
is called with ItemVec.size() == ItemVec.capacity()
, a new buffer is allocated, and all of the existing elements are moved to the new buffer ... unless your move constructor is not noexcept
, in which case all of the existing elements are copied to the new buffer.