copy constructor and initialization

Pages: 12
Greetings!

I don't understand why the copy constructor does not work for initializing an object with a return value of a function.

Example, a class "person":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
\\ HEADER

#ifndef PERSON_H
#define PERSON_H
#include <string>


class person{

    public:

        person(std::string name, int age);
        person(person& person_to_copy);

        int get_age() const;
        void set_age(int new_age);
        
        std::string get_name() const;
        void set_name(std::string new_name);


    private: 
        std::string name;
        int age;

};

person person_double_age(person someguy);  // returns the same person only with doubled age

#endif




\\ IMPLEMENTATION

#include "person.h"
#include <iostream>


person::person(std::string new_name, int new_age){
    std::cout<<"\n general constructor called";
    name = new_name;
    age = new_age;
}

person::person(person& person_to_copy){
    std::cout<<"\n copy constructor called";
    name = person_to_copy.get_name();
    age = person_to_copy.get_age();
}

int person::get_age() const{
    return age;
}

void person::set_age(int new_age){
    age = new_age;
}

std::string person::get_name() const{
    return name;
}

void person::set_name(std::string new_name){
    name = new_name;
}

person person_double_age(person someguy){
    person result_person(someguy.get_name(), someguy.get_age()*2);
    return result_person;
}



// MAIN

#include <iostream>
#include "person.h"

int main(){


    std::cout<< "\n Joe";
    person Joe {"Joe", 14};
    std::cout << "\n Joes name: " << Joe.get_name();
    std::cout << "\n Joes age: " << Joe.get_age();
    std::cout << "\n";
    
    std::cout << "\nJoe copy";
    person Joe_copy = Joe;      // calls copy constructor, works fine
    std::cout << "\n";


    std::cout << "\nJoe with double age";
    Joe = person_double_age(Joe);   // calls copy constructor when passing Joe to the function. 
                                     // depending on compiler, might need to call it 2nd time for 
                                     // storing returned value in temporary object before assignment (R-value optim).
                                     // then calls assignment operator. works fine.
    std::cout<<"\n name: "<<Joe.get_name();
    std::cout<<"\n age: "<<Joe.get_age();
    std::cout<<"\n";


    return 0;
}



This gives the expected output
1
2
3
4
5
6
7
8
9
10
11
12
13
Joe
general constructor called
Joes name: Joe
Joes age: 14

Joe copy
copy constructor called

Joe with double age
copy constructor called
general constructor called
name: Joe
age: 28


However, if I try to do this
person Joe2 = person_double_age(Joe); ,
I expect to see another call of the copy constructor due to the "=" operator used for initialization.
Instead, I get an error:
error: cannot bind non-const lvalue reference of type ‘person&’ to an rvalue of type ‘person’
What's going on?

Best,
PiF
Last edited on
Hello. I don't understand where is the problem. It works as expected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <iostream>
#include <string>

class person {

public:
    person(std::string name, int age);
    person(person& person_to_copy);

    int get_age() const;
    void set_age(int new_age);

    std::string get_name() const;
    void set_name(std::string new_name);

private:
    std::string name;
    int age;

};

person::person(std::string new_name, int new_age) {
    std::cout << "\n general constructor called";
    name = new_name;
    age = new_age;
}

person::person(person& person_to_copy) {
    std::cout << "\n copy constructor called";
    name = person_to_copy.get_name();
    age = person_to_copy.get_age();
}

int person::get_age() const {
    return age;
}

void person::set_age(int new_age) {
    age = new_age;
}

std::string person::get_name() const {
    return name;
}

void person::set_name(std::string new_name) {
    name = new_name;
}

person person_double_age(person someguy) {
    person result_person(someguy.get_name(), someguy.get_age() * 2);
    return result_person;
}

int main() 
{
    std::cout << "\nOriginal Joe with his age";
    person Joe{ "Joe", 14 };
    std::cout << "\n Joe";
    std::cout << "\n Joes name: " << Joe.get_name();
    std::cout << "\n Joes age: " << Joe.get_age();
    std::cout << "\n";

    std::cout << "\nJoe with double age";
    Joe = person_double_age(Joe);       
    std::cout << "\n name: " << Joe.get_name();
    std::cout << "\n age: " << Joe.get_age();
    std::cout << "\n";

    std::cout << "\nNew Joe copy with double age";
    person Joe2 = person_double_age(Joe);
    std::cout << "\n name: " << Joe2.get_name();
    std::cout << "\n age: " << Joe2.get_age();
    std::cout << "\n";

    return 0;
}



Original Joe with his age
 general constructor called
 Joe
 Joes name: Joe
 Joes age: 14

Joe with double age
 copy constructor called
 general constructor called
 copy constructor called
 name: Joe
 age: 28

New Joe copy with double age
 copy constructor called
 general constructor called
 copy constructor called
 name: Joe
 age: 56
Last edited on
Ok. I found the problem.
You are trying to run this code under ISO C++14 (/std:c++14) compiler parameter.
It works at Norm ISO C++17 (/std:c++17) at least. Try it on the cpp.sh link under my script. Play with the compiler parameters...

https://docs.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version?view=msvc-170

Change your project properties this way :
project >> properties >> C/C++ >> language >> language norm

I hope that it helps you ++
Last edited on
Hi Geckoo,

thanks or the answer.

I am compiling from the terminal using gnu. I tried using the -std=c++17 flag, but it does not work, still gives the same error :( .
Last edited on
$ g++ --version
g++ (GCC) 11.2.1 20220127 (Red Hat 11.2.1-9)
man g++ writes:
gnu++17
GNU dialect of -std=c++17. This is the default for C++ code.

$ g++ -Wall -Wextra -o foo foo.cpp 
foo.cpp: In function ‘int main()’:
foo.cpp:88:32: warning: implicitly-declared ‘person& person::operator=(const person&)’ is deprecated [-Wdeprecated-copy]
   88 |     Joe = person_double_age(Joe);   // calls copy constructor when passing Joe to the function.
      |                                ^
foo.cpp:42:1: note: because ‘person’ has user-provided ‘person::person(person&)’
   42 | person::person(person& person_to_copy){
      | ^~~~~~

Doesn't that say: "If you define copy ctor, then you have to define copy assignment too."

Add member:
person& operator= (const person& ) = default;

$ g++ -Wall -Wextra -o foo foo.cpp
$ ./foo

 Joe
 general constructor called
 Joes name: Joe
 Joes age: 14

Joe copy
 copy constructor called

Joe with double age
 copy constructor called
 general constructor called
 name: Joe
 age: 28
Oh. I am Using Visual Studio under Windows. Do you see Edit & Run on cpp.sh tab under my script? Test it. It works with C++17 at least - non with C++14. Maybe you got some specificities with your platform. Sorry for the burst ballon ++
Last edited on
If you provide a operator=() function, then you'll find that it's called for Joe = person_double_age(Joe) as expected.

The issue is when does the compiler provide a default operator=() function? As there's no dynamic memory involved, the default operator=() will be fine. The same as there's no need to provide a default copy constructor.

From MSDN:

"Additionally, the C++11 standard specifies the following additional rules:

If a copy constructor or destructor is explicitly declared, then automatic generation of the copy-assignment operator is deprecated.
If a copy-assignment operator or destructor is explicitly declared, then automatic generation of the copy constructor is deprecated.

In both cases, Visual Studio continues to automatically generate the necessary functions implicitly, and does not emit a warning."

https://docs.microsoft.com/en-us/cpp/cpp/explicitly-defaulted-and-deleted-functions?view=msvc-170

So in this case, this is a MS C++ language extension...
Last edited on

In both cases, Visual Studio continues to automatically generate the necessary functions implicitly, and does not emit a warning.

So in this case, this is a MS C++ language extension...

Easy way :)
Your copy constructor is incorrect and is different from the generated default copy constructor.
When it comes to implementing class copy constructors/destructor it is useful to remember "The Rule of Three (or Five)".

https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming)

Please note this is not a Must Do rule, it is a good suggestion, unless circumstances dictate otherwise.

There is also "The Rule of Zero" that is a good idea to follow. As the C++ Core Guidelines suggests:

C.20: If you can avoid defining any default operations, do
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-zero
Last edited on
The arg to copy-constructor would usually be const ref - not just ref.

Also string is usually passed by const ref - not by value - to avoid an unnecessary copy. If string is passed by value, then use std::move() when assigning (some prefer this way).
Can we suggest member initializer lists too?
1
2
3
4
5
6
7
8
9
10
11
person::person(const std::string& name, int age)
 : name{name}, age{age}
{
    std::cout << "\n general constructor called";
}

person::person(const person& rhs)
 : name{rhs.name}, age{rhs.age}
{
    std::cout << "\n copy constructor called";
}

Ok, one could use getters, but they are trivial here and person can access privates directly.
Easy way :)


Personally I don't like the MS extension. If you have a custom copy-constructor then usually this is because you're doing something special (eg using dynamic memory) and the simple shallow default copy is not sufficient. In this case you also need a custom assignment as well. And having a default assignment in this case is probably wrong and very likely to give run-time issues. I would much prefer having the compiler kick up a fuss about a missing assignment (easily fixed if the default one is actually OK and required) then have a run-time issue which could be hard to track down. Always prefer a compiler error than a run-time issue. Just my 2-cents.
Last edited on
Always prefer a compiler error than a run-time issue.

Wisdom. It makes sense.
seeplus wrote:
Always prefer a compiler error than a run-time issue.

Sage advice, quoting/paraphrasing the C++ Core Guidelines:

P.5: Prefer compile-time checking to run-time checking
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-compile-time

There is a good followup to that:

P.6: What cannot be checked at compile time should be checkable at run time
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-run-time
Hi everyone, sorry for the late reaction, and thanks for your many comments!

I didn't think about specifying an assignment operator (as it was not mentioned in my book at this point).

Following the hints of @kbw and @seeplus
Your copy constructor is incorrect and is different from the generated default copy constructor.

The arg to copy-constructor would usually be const ref - not just ref.


the code person Joe2 = person_double_age(Joe); worked even without defining an assign operator.

The output, however, is
1
2
copy constructor called
 general constructor called

The copy constructor is called once to pass a person to the function. The general constructor is called within the function.
But I thought there should be a second call to the copy constructor due to the = used for initialization.

I mean, the copy constructor is called here:
1
2
    person Joe {"Joe", 14};
    person Joe_copy = Joe;      // calls copy constructor, works fine 

so why not in my example?
Last edited on
The assignment operator is used when you assign to an already existing object.
The copy constructor is used when you create a new object that is a copy of another object.
Whether or not you use the = symbol doesn't matter.
Last edited on
With some code changes, consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <iostream>
#include <string>
#include <utility>

class person {

public:
	person(const std::string& name, int age);
	person(const person& person_to_copy);
	person& operator=(const person per);

	int get_age() const;
	void set_age(int new_age);

	std::string get_name() const;
	void set_name(const std::string& new_name);

private:
	std::string name;
	int age {};

};

person::person(const std::string& new_name, int new_age) : name(new_name), age(new_age) {
	std::cout << "\ngeneral constructor called";
}

person::person(const person& person_to_copy) : person(person_to_copy.name, person_to_copy.age) {
	std::cout << "\ncopy constructor called";
}

person& person::operator=(person per) {
	std::swap(per.age, age);
	std::swap(per.name, name);
	std::cout << "\nassignment copy called\n";
	return *this;
}

int person::get_age() const {
	return age;
}

void person::set_age(int new_age) {
	age = new_age;
}

std::string person::get_name() const {
	return name;
}

void person::set_name(const std::string& new_name) {
	name = new_name;
}

person person_double_age(person someguy) {
	someguy.set_age(someguy.get_age() * 2);
	return someguy;
}

int main() {
	std::cout << "\nOriginal Joe with his age";

	person Joe { "Joe", 14 };

	std::cout << "\n Joe";
	std::cout << "\n Joes name: " << Joe.get_name();
	std::cout << "\n Joes age: " << Joe.get_age();
	std::cout << "\n";

	std::cout << "\nJoe with double age";
	Joe = person_double_age(Joe);
	std::cout << "\n name: " << Joe.get_name();
	std::cout << "\n age: " << Joe.get_age();
	std::cout << "\n";

	std::cout << "\nNew Joe copy with double age";

	person Joe2 { person_double_age(Joe) };

	std::cout << "\n name: " << Joe2.get_name();
	std::cout << "\n age: " << Joe2.get_age();
	std::cout << "\n";
}


which displays:



Original Joe with his age
general constructor called
 Joe
 Joes name: Joe
 Joes age: 14

Joe with double age
general constructor called
copy constructor called
general constructor called
copy constructor called
assignment copy called

 name: Joe
 age: 28

New Joe copy with double age
general constructor called
copy constructor called
general constructor called
copy constructor called
 name: Joe
 age: 56

Peter87 wrote:


The assignment operator is used when you assign to an already existing object.
The copy constructor is used when you create a new object that is a copy of another object.
Whether or not you use the = symbol doesn't matter.

I understand, but this doesn't answer my questions. In both cases I had an initialization:
person Joe2 = person_double_age(Joe); // copy constructor is called only once (passing to function)
It should be called a second time for the initialization of Joe2.

1
2
person Joe {"Joe", 14};
person Joe_copy = Joe;  // copy constructor is called 

Here, it is called for the initialization.
Isn't this behaviour inconsistent?

@seeplus
The behaviour of your second case confuses me.
1
2
3
4
5
std::cout << "\nJoe with double age";
	Joe = person_double_age(Joe);
	std::cout << "\n name: " << Joe.get_name();
	std::cout << "\n age: " << Joe.get_age();
	std::cout << "\n";


Output
1
2
3
4
5
6
Joe with double age
general constructor called
copy constructor called
general constructor called
copy constructor called
assignment copy called

Why is the copy constructor called twice?
From my point of view, only the first one makes sense (pass argument to function).
Afterwards, I only need the assignment operator, no?




L32 - passed by value
L55 - passed by value

pass by value -> copy -> copy constructor. Hence 2 copy constructors.

But add a move constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <iostream>
#include <string>
#include <utility>
#include <iterator>

class person {

public:
	person(std::string name, int age);
	person(const person& person_to_copy);
	person(person&& person_to_move);
	person& operator=(const person per);

	int get_age() const;
	void set_age(int new_age);

	std::string get_name() const;
	void set_name(const std::string& new_name);

private:
	std::string name;
	int age {};
};

person::person(std::string new_name, int new_age) : name(std::move(new_name)), age(new_age) {
	std::cout << "\ngeneral constructor called";
}

person::person(const person& person_to_copy) : person(person_to_copy.name, person_to_copy.age) {
	std::cout << "\ncopy constructor called";
}

person::person(person&& person_to_move) : person(std::move(person_to_move.name), person_to_move.age) {
	std::cout << "\nmove constructor called";
}

person& person::operator=(person per) {
	std::swap(per.age, age);
	std::swap(per.name, name);
	std::cout << "\nassignment copy called\n";
	return *this;
}

int person::get_age() const {
	return age;
}

void person::set_age(int new_age) {
	age = new_age;
}

std::string person::get_name() const {
	return name;
}

void person::set_name(const std::string& new_name) {
	name = new_name;
}

person person_double_age(person someguy) {
	someguy.set_age(someguy.get_age() * 2);
	return someguy;
}

int main() {
	std::cout << "\nOriginal Joe with his age";

	person Joe { "Joe", 14 };

	std::cout << "\n Joe";
	std::cout << "\n Joes name: " << Joe.get_name();
	std::cout << "\n Joes age: " << Joe.get_age();
	std::cout << "\n";

	std::cout << "\nJoe with double age";
	Joe = person_double_age(Joe);
	std::cout << "\n name: " << Joe.get_name();
	std::cout << "\n age: " << Joe.get_age();
	std::cout << "\n";

	std::cout << "\nNew Joe copy with double age";

	person Joe2 { person_double_age(Joe) };

	std::cout << "\n name: " << Joe2.get_name();
	std::cout << "\n age: " << Joe2.get_age();
	std::cout << "\n";
}


and you get:


Original Joe with his age
general constructor called
 Joe
 Joes name: Joe
 Joes age: 14

Joe with double age
general constructor called
copy constructor called
general constructor called
move constructor called
assignment copy called

 name: Joe
 age: 28

New Joe copy with double age
general constructor called
copy constructor called
general constructor called
move constructor called
 name: Joe
 age: 56


Pages: 12