Home > Enterprise >  How to know when a copy is being created?
How to know when a copy is being created?

Time:06-21

I'm trying to get better at understanding the intricacies of C . I came across this exercise in C Best Practices by Jason Turner.

The question is if a copy is being created here.

std::map<std::string, int> get_map();
using element_type = std::pair<std::string, int>;
for (const element_type & : get_map())
{
}

My answer is yes, because I think std::map isn't explicitly using std::pair to represent the key-value pair. So maybe implicitly the underlying key-value pair is being copied to element_type? How off am I here? Any help would be appreciated :).

CodePudding user response:

i try this simple code section:

#include <map>
#include <string>
#include <iostream>

std::map<std::string, int> get_map()
{
    std::map<std::string, int> m { {"CPU", 10}, {"GPU", 15} };
  
  return m;
}

int main()
{
    using element_type = std::pair<std::string, int>;
    for (const element_type &e : get_map())
    {
    }
    return 1;
}

It does NOT copy, insteady the move is moved. i use https://cppinsights.io/ to get:

std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > > get_map()
{
  std::map<std::string, int> m = std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > >{std::initializer_list<std::pair<const std::basic_string<char>, int> >{std::pair<const std::basic_string<char>, int>{"CPU", 10}, std::pair<const std::basic_string<char>, int>{"GPU", 15}}, std::less<std::basic_string<char> >(), std::allocator<std::pair<const std::basic_string<char>, int> >()} /* NRVO variable */;
  return std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > >(static_cast<std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > > &&>(m));
}


int main()
{
  using element_type = std::pair<std::basic_string<char>, int>;
  {
    std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > > && __range1 = get_map();
    std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, int> > __begin1 = __range1.begin();
    std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, int> > __end1 = __range1.end();
    for(; operator!=(__begin1, __end1); __begin1.operator  ()) {
      const std::pair<std::basic_string<char>, int> & e = std::pair<std::basic_string<char>, int>(__begin1.operator*());
    }
    
  }
  return 1;
}

Hope this help. thx.

CodePudding user response:

The for loop will use an iterator internally. Dereferencing an iterator will generate a value_type object not a reference to one. So even though your own loop specifies a reference, it will be a reference to a temporary object that was created (via copy) by the iterator and kept alive by your reference.

CodePudding user response:

In my answer, I'd like to address the question title literally:

How to know when a copy is being created?

This can be achieved quite easy with a sample class where every constructor in quest makes an output:

struct Data {
  Data() { std::cout << "Data::Data()\n"; }
  Data(const Data&) { std::cout << "Data::Data(const Data&)\n"; }
  Data& operator=(const Data&) { std::cout << "Data::operator=(const Data&)\n"; return *this; }
  Data(Data&&) { std::cout << "Data::Data(Data&&)\n"; }
  Data& operator=(Data&&) { std::cout << "Data::operator=(Data&&)\n"; return *this; }
};

According to cppreference.com, the copy constructor of std::pair is the default, i.e. it just calls the copy constructors of its member variables:

pair( const pair& p ) = default;

Thus, a little MCVE can illustrate the mistake of the OP quite obviously:

#include <iostream>
#include <map>

struct Data {
  Data() { std::cout << "Data::Data()\n"; }
  Data(const Data&) { std::cout << "Data::Data(const Data&)\n"; }
  Data& operator=(const Data&) { std::cout << "Data::operator=(const Data&)\n"; return *this; }
  Data(Data&&) { std::cout << "Data::Data(Data&&)\n"; }
  Data& operator=(Data&&) { std::cout << "Data::operator=(Data&&)\n"; return *this; }
};

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ ; std::cout << std::endl

int main()
{
  DEBUG(std::map<int, Data> aMap = { { 1, Data() }, { 2, Data() } });
  // done like OP -> copy assignment
  DEBUG(for (const std::pair<int, Data>& value : aMap) {
    std::cout << value.first << '\n';
  });
  // without copy assignment
  DEBUG(for (const std::pair<const int, Data>& value : aMap) {
    std::cout << value.first << '\n';
  });
  DEBUG(for (const std::map<int, Data>::value_type& value : aMap) {
    std::cout << value.first << '\n';
  });
}

Output:

std::map<int, Data> aMap = { { 1, Data() }, { 2, Data() } };
Data::Data()
Data::Data(Data&&)
Data::Data()
Data::Data(Data&&)
Data::Data(const Data&)
Data::Data(const Data&)

for (const std::pair<int, Data>& value : aMap) { std::cout << value.first << '\n'; };
Data::Data(const Data&)
1
Data::Data(const Data&)
2

for (const std::pair<const int, Data>& value : aMap) { std::cout << value.first << '\n'; };
1
2

for (const std::map<int, Data>::value_type& value : aMap) { std::cout << value.first << '\n'; };
1
2

Demo on coliru

Thus, for the first loop (using std::pair<int, Data>) the copy constructor of Data is called in each iteration.

This doesn't happen for std::pair<const int, Data>
nor for the std::map<int, Data>::value_type.

Another simple alternative could have been the usage of auto:

  for (const auto& value : aMap) {
    std::cout << value.first << '\n';
  }

Furthermore, I would like to mention the diagnostic of g with an even shorter MCVE:

#include <iostream>
#include <map>

struct Data { };

int main()
{
  std::map<int, Data> aMap = { { 1, Data() } };
  for (const std::pair<int, Data>& value : aMap) std::cout << value.first << '\n';
}

Output:

main.cpp: In function 'int main()':
main.cpp:9:36: warning: loop variable 'value' of type 'const std::pair<int, Data>&' binds to a temporary constructed from type 'std::pair<const int, Data>' [-Wrange-loop-construct]
    9 |   for (const std::pair<int, Data>& value : aMap) std::cout << value.first << '\n';
      |                                    ^~~~~
main.cpp:9:36: note: use non-reference type 'const std::pair<int, Data>' to make the copy explicit or 'const std::pair<const int, Data>&' to prevent copying

Demo on coliru

  •  Tags:  
  • c
  • Related