Home > Blockchain >  efficient use of std::move to aggregate all object instances in a std container
efficient use of std::move to aggregate all object instances in a std container

Time:12-28

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.

  • Related