1

I am trying to understand the concept of copy and move constructors in C++. And so trying out different examples. One such example whose output i am unable to understand is given below:

#include <iostream>
#include <vector>
using namespace std;
struct NAME 
{
    NAME()
    {
        std::cout<<"default"<<std::endl;
    }
    NAME(const NAME& )
    {
        std::cout<<"const copy"<<std::endl;
    }
    NAME(NAME& )
    {
        std::cout<<"nonconst copy"<<std::endl;
    }
    NAME(NAME &&)
    {
        std::cout<<"move"<<std::endl;
    }
};
void foo(std::pair<std::string, NAME> ) 
{
    
}
void foo2(std::vector<std::pair<std::string, NAME>> )
{
}
int main()
{
    foo(std::make_pair("an", NAME())); //prints default --> move --> move
    std::cout << "----------------------------------------"<<std::endl;
    
    foo({"an", NAME()});               //prints default --> move
    std::cout << "----------------------------------------"<<std::endl;
    
    foo2({{"an", NAME()}});            //prints default --> move --> const copy
    std::cout << "----------------------------------------"<<std::endl;
    return 0;
}

Case 1: For foo(std::make_pair("an", NAME()));

Output

default
move
move

This is what i think is happening.

Step 1. A temporary of type NAME() is craeted because we've passed it to std::make_pair using the NAME()'s default constructor.

Step 2. One of the std::make_pair's constructor is used which forwards(moves) the the temporary that was created in the 1st step. So NAME()'s move constructor.

Step 3. Finally, since the argument to foo is passed by value, so the std::pair that was created in step 2 is "moved"(not "copied"?) which in turn "moves" the NAME temporary one last time.

Case 2: For foo({"an", NAME()});

Output

default
move

Step 1. A temporary NAME is created.

Step 2. This time since we do not have std::make_pair, std::pair's initializer list constructor(if any) is used to "move" the temporary created in step 1.

Case 3: For foo2({{"an", NAME()}});

Output

default
move
const copy

I have no idea why copy constructor is used instead of move constructor and also why the const version is used instead of nonconst version of copy constructor.

Are my explanation correct. Please correct me where i am wrong in any of my explanation steps in detail.

  • 3
    A non-const copy constructor shouldn’t be a thing. – sweenish Nov 13 '21 at 13:37
  • @sweenish Believe it or not, non-const copy constructor is a C++ thing. Any constructor for a class T that has one mandatory argument of type `T &` or `T const &` (it may also have further, defaulted arguments) is a copy constructor. –  Nov 13 '21 at 13:43
  • Does this answer your question? [Why copy constructor is called in std::vector's initializer list?](https://stackoverflow.com/questions/36420829/why-copy-constructor-is-called-in-stdvectors-initializer-list) – JaMiT Nov 13 '21 at 13:59
  • For the related question of how to get around this: [Can I list-initialize a vector of move-only type?](https://stackoverflow.com/questions/8468774/) and [Can I list-initialize std::vector with perfect forwarding of the elements?](https://stackoverflow.com/questions/59655413/) – JaMiT Nov 13 '21 at 14:01
  • @JaMiT But if initilalizer_list uses copy constructor then why in Case 2's point 2 move constructor is used? That is, in my explanation of Case 2 look at point 2. There also `std::pair`'s initializer_list constructor is used then there also the copy constructor should be used. –  Nov 13 '21 at 14:05
  • Also, from the past hour or so: [How to avoid copying when creating a vector argument from an initializer list](https://stackoverflow.com/questions/69954424/) – JaMiT Nov 13 '21 at 14:10
  • How do you imagine case 2 constructs a *vector*? Where is the vector in case 2? – JaMiT Nov 13 '21 at 14:13
  • @JaMiT There is no vector in Case 2. But there `std::pair`'s intializer_list constructor is used. And according to the answer given by yurikilochek, elements of `std::inializer_list` are `const` and so cannot be moved. This means the temporary `NAME()` is also `const`. So in case 2 the const copy constructor should be used instead of move constructor. –  Nov 13 '21 at 14:20
  • I am not yuri kilochek; requests for clarification of an answer should be directed to the answer's author. I have no additional insight to offer. I found other questions (with answers) related to initializing vectors. You asked about initializing a vector. QED. If you want to ask about initializing a pair, you can ask a new question after checking to see if it's already been asked and answered. – JaMiT Nov 13 '21 at 14:44
  • @AanchalSharma `std::pair` has nothing to do with `std::initializer_list`. An expression in curly braces does not `std::initializer_list` is involved. – yuri kilochek Nov 13 '21 at 14:51
  • I wasn't speaking to whether you can write one or not. *Shouldn't* implies poor practices in this case. C++ will always let you shoot yourself in the foot. – sweenish Nov 13 '21 at 18:32

2 Answers2

0

Elements of std::initializer_list, are const and cannot be moved from.

yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
0

Case 1: For foo(std::make_pair("an", NAME()));

Output

default
move
move

The crucial issue here is to understand what is the type of the above make_pair invocation. It is not a std::pair<std::string, NAME> as I think you believe, but a std::pair<const char *, NAME> !

You can confirm this by checking that this prints 1.

std::cout << std::is_same_v< decltype(std::make_pair("an", NAME()))
                           , std::pair<const char *, NAME>> << "\n";

So, we observe that:

  1. NAME is constructed using the default constructor. (output: default)
  2. make_pair moves that to return its pair (output: move)
  3. We can not pass that pair to foo, which instead expects std::pair<std::string, NAME>. An implicit pair conversion constructor is called, which creates the string and moves the NAME once again. (output: move)

This is why we observe two moves instead of just one.

Case 2: For foo({"an", NAME()});

Output

default
move

Here we don't call a function to make the pair, so "an" is used to initialize the needed pair directly, which has the right std::pair<std::string, NAME>. We no longer create a pair with the "wrong" type, hence one fewer move.

On case 3: I could not understand why the move constructor is called in case 3. I thought that in order to allow vector to move the NAME, it would have sufficed to mark the move constructor as noexcept, but even in that case the copy is performed. I have no idea right now.

It looks like the move constructor of std::pair is not (conditionally) noexcept as one would expect. That might be the underlying cause.

chi
  • 111,837
  • 3
  • 133
  • 218
  • Can you tell me which among [these](https://en.cppreference.com/w/cpp/utility/pair/pair) is used? Or is it aggregate initialization. –  Nov 13 '21 at 17:38
  • @AanchalSharma I wish I knew for sure. My guess is that in case 2 it's copy-list-initialization, calling (3). In case 1, we instead call (5) as an implicit. – chi Nov 13 '21 at 17:58