Puzzle with C++ constructor and overloaded operator

In summary: ThreeVector a(1, 2, 3), b(4, 5, 6), c(7, 8, 9);...with the addition...c = a + b;...to see what happens:Declare a and b...-- Constructed vector #1 containing (1, 2, 3), (4, 5, 6) and (7, 8, 9).Declare c...-- Default-constructed vector #2 containing (0, 0, 0), (4, 5, 6) and (7, 8, 9).Add c = a + b...-- Default-constructed vector #3 containing (0, 0, 0
  • #1
jtbell
Staff Emeritus
Science Advisor
Homework Helper
15,927
5,739
TL;DR Summary
A constructor doesn't seem to be invoked when it should be.
I mentioned in another thread that I've been playing with a C++ class for 3D vectors, to review overloaded operators, constructors, etc. All those functions display output so I can follow which ones get called, and when. The class generates a serial number for each vector so I can tell them apart.

I've been puzzled by something. To illustrate it, here's a trimmed-down version of the class that contains only the operations needed for this demonstration.
C++:
class ThreeVector
{
private:
    double x, y, z;
    int serialNum;          // unique for each ThreeVector instance
    static int numVectors;  // counts the number of ThreeVectors constructed
public:
    ThreeVector ();                        // default constructor
    ThreeVector (double, double, double);
    ThreeVector (const ThreeVector&);      // copy constructor
    ~ThreeVector ();
    const ThreeVector operator+ (const ThreeVector&) const;
    ThreeVector& operator= (const ThreeVector&);
    friend ostream& operator<< (ostream& out, const ThreeVector& v);
};

int ThreeVector::numVectors = 0;

ThreeVector::ThreeVector ()
{
    x = 0;  y = 0;  z = 0;
    numVectors++;
    serialNum = numVectors;
    cout << "-- Default-constructed vector #" << serialNum 
         << " containing " << *this << "." << endl;
}

ThreeVector::ThreeVector (double x0, double y0, double z0)
{
    x = x0;  y = y0;  z = z0;
    numVectors++;
    serialNum = numVectors;
    cout << "-- Constructed vector #" << serialNum << " containing "
         << *this << "." << endl;
}

ThreeVector::ThreeVector (const ThreeVector& original)
{
    x = original.x;  y = original.y;  z = original.z;
    numVectors++;
    serialNum = numVectors;
    cout << "-- Copy-constructed vector #" << serialNum << " containing "
         << *this << "." << endl;
}

ThreeVector::~ThreeVector ()
{
    cout << "-- Destructed vector #" << serialNum << " containing "
         << *this << "." << endl;   
}

const ThreeVector ThreeVector::operator+ (const ThreeVector& rhs) const
{
    ThreeVector sum;
    sum.x = x + rhs.x;  sum.y = y + rhs.y;  sum.z = z + rhs.z;
    cout << "-- " << *this << " + " << rhs 
         << " = " << sum << "." << endl;
    cout << "Return from operator+..." << endl;
    return sum;
}

ThreeVector& ThreeVector::operator= (const ThreeVector& rhs)
{
    x = rhs.x;
    y = rhs.y;
    z = rhs.z;
    cout << "-- Assigned " << *this << " to vector #" << serialNum 
         << "." << endl;
    return *this;
}

ostream& operator<< (ostream& out, const ThreeVector& v)
{
    out << "(" << v.x << ", " << v.y << ", " << v.z << ")";
    return out;
}
First, here's a main() function that works as I expected:
C++:
int main ()
{
    cout << "Declare a and b..." << endl;
    ThreeVector a(1, 2, 3), b(4, 5, 6);
    cout << "Declare c..." << endl;
    ThreeVector c;
    cout << "Add c = a + b..." << endl;
    c = a + b;
    cout << "Return from main()..." << endl;
    return 0;
}
and its output:
Code:
Declare a and b...
-- Constructed vector #1 containing (1, 2, 3).
-- Constructed vector #2 containing (4, 5, 6).
Declare c...
-- Default-constructed vector #3 containing (0, 0, 0).
Add c = a + b...
-- Default-constructed vector #4 containing (0, 0, 0).
-- (1, 2, 3) + (4, 5, 6) = (5, 7, 9).
Return from operator+...
-- Assigned (5, 7, 9) to vector #3.
-- Destructed vector #4 containing (5, 7, 9).
Return from main()...
-- Destructed vector #3 containing (5, 7, 9).
-- Destructed vector #2 containing (4, 5, 6).
-- Destructed vector #1 containing (1, 2, 3).
Vectors #1, #2 and #3 are a, b, and c in main(). They are destructed at the end of main().

Vector #4 is the local variable sum in operator+(). It is destructed when that function finishes, after its value is assigned (copied) to c in main().

This all seems reasonable to me.

Now, let's combine the declaration of c with the addition of a and b:
C++:
int main ()
{
    cout << "Declare a and b..." << endl;
    ThreeVector a(1, 2, 3), b(4, 5, 6);
    cout << "Declare c and add c = a + b..." << endl;
    ThreeVector c = a + b;
    cout << "Return from main()..." << endl;
    return 0;
}
Based on my (possibly faulty) memory of how C++ compilers are supposed to handle this, I expected that this would bypass operator=() and use the copy-constructor to construct c.

Output:
Code:
Declare a and b...
-- Constructed vector #1 containing (1, 2, 3).
-- Constructed vector #2 containing (4, 5, 6).
Declare c and add c = a + b...
-- Default-constructed vector #3 containing (0, 0, 0).
-- (1, 2, 3) + (4, 5, 6) = (5, 7, 9).
Return from operator+...
Return from main()...
-- Destructed vector #3 containing (5, 7, 9).
-- Destructed vector #2 containing (4, 5, 6).
-- Destructed vector #1 containing (1, 2, 3).
Indeed, operator=() was not used, but neither was the copy-constructor!

What is vector #3?

If it's c in main(), then why is there no vector #4 for the local sum in operator+()?

If it's the local sum in operator+(), then how does c in main() get constructed? Is there another constructor that I've overlooked?

If the compiler is doing some optimization, they why is it happening only in the second program and not the first one?
 
Technology news on Phys.org
  • #2
If you are using C++ 17 or later there is something called copy elision which would explain what you see.
 
  • #3
jtbell said:
C++:
ThreeVector c = a + b;
Based on my (possibly faulty) memory of how C++ compilers are supposed to handle this, I expected that this would bypass operator=() and use the copy-constructor to construct c.
I don't think so, at least if I understand your question following a quick read of it. What I believe is happening (without copying your code and compiling it on my own machine) is this:
  1. operator+() is called to create a temp object that is the sum of the a and b objects,
  2. the default constructor is called to create c,
  3. the operator=() overload is called to assign the values in the temp object to those of c.
A copy constructor would be called like so:
ThreeVector c(a);
 
  • #4
Mark44 said:
3. the operator=() overload is called to assign the values in the temp object to those of c.

But as jtbell wrote, according to the program output the assignment operator was not called.
 
  • #5
I think I understand what's going on now. Here's a snippet of the revised code from post #1:
C++:
ThreeVector a(1, 2, 3), b(4, 5, 6);
cout << "Declare c and add c = a + b..." << endl;
ThreeVector c = a + b;

If we add a couple of lines as in the following, then the overloaded operator=() gets called in the last line.
C++:
ThreeVector a(1, 2, 3), b(4, 5, 6);
cout << "Declare c and add c = a + b..." << endl;
ThreeVector c = a + b;
ThreeVector d;
d = a;

The difference in what gets called between the c and d objects is that c has to be constructed and then given values, via the copy constructor, whereas d is constructed, and then subsequently has values assigned to it via operator+().

The definition of c can be rewritten as ThreeVector c(a + b); with no change in the program output.
 
  • #6
I was going to write much what Mark did. (Via experimenting)
 
  • #7
Mark44 said:
The difference in what gets called between the c and d objects is that c has to be constructed and then given values, whereas d is constructed, and then subsequently has values assigned to it.

I agree on that as well, with the added comment that "given values" is done in a way, as jtbell observes, that neither use the copy constructor (as it would for earlier language versions) nor the assignment operator. To me it sounds like jtbell are well aware (correct me if I'm wrong) of the difference between c and d, but wants to know how c is actually constructed since it apparently is not via a temporary object that is assigned or copied in.

Or in other words, for the question:
jtbell said:
Is there another constructor that I've overlooked?
I would still say that with the given information the answer is, yes, for C++ 17+ copy eilision will be applied in certain situations such that objects are constructed without invoking any user defined copy constructor or assignment operator.
 
  • #8
Filip Larsen said:
If you are using C++ 17 or later there is something called copy elision which would explain what you see.
This, or something similar, appears to be the answer. I say "something similar" because I don't think my compiler uses C++17 by default. I think it defaults to something below C++11, because I have to use a command-line option to choose C++11 in order to use features introduced in that standard. I haven't tried specifying C++17 yet.

As a test, I modified my operator+() so it initializes the local sum with a nonzero value, then skips doing the actual addition, and simply returns the initialized value;

C++:
const ThreeVector ThreeVector::operator+ (const ThreeVector& rhs) const
{
    ThreeVector sum(1,1,1);
//  sum.x = x + rhs.x;  sum.y = y + rhs.y;  sum.z = z + rhs.z;
    cout << "-- " << *this << " + " << rhs
         << " = " << sum << "." << endl;
    cout << "Return from operator+..." << endl;
    return sum;
}
And I added a statement in main() to print the value of c after the declaration+initialization:
C++:
int main ()
{
    cout << "Declare a and b..." << endl;
    const ThreeVector a(1, 2, 3), b(4, 5, 6);
    cout << "Declare c and add c = a + b..." << endl;
    ThreeVector c = a + b;
    cout << "Back in main(), c = " << c << "." << endl;
    cout << "Return from main()..." << endl;
    return 0;
}
Output:
Code:
Declare a and b...
-- Constructed vector #1 containing (1, 2, 3).
-- Constructed vector #2 containing (4, 5, 6).
Declare c and add c = a + b...
-- Constructed vector #3 containing (1, 1, 1).
-- (1, 2, 3) + (4, 5, 6) = (1, 1, 1).
Return from operator+...
Back in main(), c = (1, 1, 1).
Return from main()...
-- Destructed vector #3 containing (1, 1, 1).
-- Destructed vector #2 containing (4, 5, 6).
-- Destructed vector #1 containing (1, 2, 3).
So vector #3 is both c in main() and sum in operator+(). I suppose one could say that sum behaves like a reference to c, even though it's not declared as such.
 
  • #9
jtbell said:
This, or something similar, appears to be the answer. I say "something similar" because I don't think my compiler uses C++17 by default. I think it defaults to something below C++11, because I have to use a command-line option to choose C++11 in order to use features introduced in that standard. I haven't tried specifying C++17 yet.

If you are using C++11 (or a partial subset which is not uncommon even if the compiler isn't configured for full C++11 support) then I would expect the assignment to the c variable to be done using the Move Constructor since the temporary a+b object will just go out of scope otherwise and thus it is available for the move operation.

https://en.cppreference.com/w/cpp/language/move_constructor
 
  • #10
glappkaeft said:
I would expect the assignment to the c variable to be done using the Move Constructor

That is also what I for years have been thinking happens on initializations like the present case, but I am not so sure anymore after rereading the fine print of the move constructor semantics saying that an implicit move constructor is only added if there are no user defined copy constructor. As I understand it, the move constructor and its semantics was introduced with C++ 11 and has (largely) remained unchanged.
 
  • Informative
Likes glappkaeft
  • #11
My general feeling on such things is that it is a) in general a bad idea to rely on "side effects" (and I would consider the keeping track of object number a side effect) and b) it is also in general a bad idea to rely on the code being implemented in a certain way by the compiler. Combining the two can produce...er...puzzles to delight and amuse.

If I wanted to keep track of objects, I would probably assign them a serial number at the Factory.
 
  • Like
Likes pbuk
  • #12
True there should not be an implicit move constructor in this case (I did read the cpp reference page properly but didn't check the code well enough). This test (from the Wikipedia article on Copy Elision) should be a good test to see of copy elision is being performed. The test output above is almost the same test so I think @Filip Larsen is right about Copy Elision.

Code:
#include <iostream>

int n = 0;

struct C {
  explicit C(int) {}
  C(const C&) { ++n; }  // the copy constructor has a visible side effect
};                      // it modifies an object with static storage duration

int main() {
  C c1(42);      // direct-initialization, calls C::C(42)
  C c2 = C(42);  // copy-initialization, calls C::C(C(42))

  std::cout << n << std::endl;  // prints 0 if the copy was elided, 1 otherwise
}
 
  • #13
Selected portions from the cppreference page on Copy Elision. I think both these quotes matches the situation very well.

Non-mandatory elision of copy/move (since C++11) operations
<SNIP>
  • In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring cv-qualification) as the target object. When the nameless temporary is the operand of a return statement, this variant of copy elision is known as RVO, "return value optimization".
(until C++17)
Return value optimization is mandatory and no longer considered as copy elision; see above.(since C++17)

and

Notes
Copy elision is the only allowed form of optimization (until C++14)one of the two allowed forms of optimization, alongside allocation elision and extension, (since C++14) that can change the observable side-effects. Because some compilers do not perform copy elision in every situation where it is allowed (e.g., in debug mode), programs that rely on the side-effects of copy/move constructors and destructors are not portable.

Edit: So in summary in C++17 the compiler must use Copy Elision in this case and in earlier version it is allowed to use it.
 
  • Like
Likes Filip Larsen
  • #14
In the meantime, I tried a couple of things. First, I added a move constructor to my class. I guessed that I could make it identical to the copy constructor except for the signature and the output comment, because the class doesn't use dynamic memory allocation.
C++:
ThreeVector::ThreeVector (ThreeVector&& original)
{
    x = original.x;  y = original.y;  z = original.z;
    numVectors++;
    serialNum = numVectors;
    cout << "-- Move-constructed vector #" << serialNum << " containing "
         << *this << "." << endl;
}
At first, the compiler warned that rvalue references are a C++11 extension. Adding the -std=c++11 compiler option got rid of that message. However, the program's output remained the same as before, with no indication that the move constructor had been invoked.

Then, I noticed in the Wikipedia page about copy elision (mentioned above), the comment about g++'s -fno-elide-constructors option. I recompiled the program with it, and... bingo!
Code:
Declare a and b...
-- Constructed vector #1 containing (1, 2, 3).
-- Constructed vector #2 containing (4, 5, 6).
Declare c = a + b...
-- Constructed vector #3 containing (1, 1, 1).
-- (1, 2, 3) + (4, 5, 6) = (5, 7, 9).
Return from operator+...
-- Move-constructed vector #4 containing (5, 7, 9).
-- Destructed vector #3 containing (5, 7, 9).
-- Copy-constructed vector #5 containing (5, 7, 9).
-- Destructed vector #4 containing (5, 7, 9).
Back in main(), c = (5, 7, 9).
Return from main()...
-- Destructed vector #5 containing (5, 7, 9).
-- Destructed vector #2 containing (4, 5, 6).
-- Destructed vector #1 containing (1, 2, 3).
Now, when operator+() finishes, we have both a move constructor and a copy constructor. :wideeyed:

(While I was at it, I re-enabled the actual addition in operator+() while still initializing sum to (1,1,1).)
 
Last edited:
  • Like
Likes Filip Larsen
  • #15
I am glad that I look at this thread, I actually can follow the first post! I copy and ran the first program step by step to look at where it goes and I can understand it. Not that I have anything to contribute, just a change of paste keep studying one overload operator after the other, it is driving me crazy. Thanks for the thread.
 
  • #16
I spent a day and half on this tracing through both options in post 1. Of cause I am not expert like you guys and I am open to be wrong.....

I find the second option with ThreeVector c=a+b very straight forward. I don't see any confusion. I modified the program ONLY to display the addresses and make the stepping easier to go through the program step by step. I did not change the program. I trace and match addresses rather than names. Here is the modified Jtbell.h:
C++:
#ifndef Jtbell_H
#define Jtbell_H
#include <iostream>
using namespace std;
class ThreeVector
{private:
    double x, y, z;
    int serialNum;          // unique for each ThreeVector instance
    static int numVectors;  // counts the number of ThreeVectors constructed
public:
    ThreeVector ();                        // default constructor
    ThreeVector (double, double, double);
    ThreeVector (const ThreeVector&);      // copy constructor
    ~ThreeVector ();
    const ThreeVector operator+ (const ThreeVector&) const;
    ThreeVector& operator= (const ThreeVector&);
    friend ostream& operator<< (ostream& out, const ThreeVector& v);
};
int ThreeVector::numVectors = 0;

ThreeVector::ThreeVector ()
{ x = 0;  y = 0;  z = 0;  cout << "to default constructor,";
  numVectors++;  serialNum = numVectors;
  cout<<" create vector#"<<serialNum << ", this=" << this <<" "<<*this;
}
ThreeVector::ThreeVector (double x0, double y0, double z0)
{ x = x0;  y = y0;  z = z0;cout << "to Constructor( x0,y0,z0),";
  numVectors++; serialNum = numVectors;
  cout <<" create vector #"<< serialNum <<", this= " << this <<*this;
}
ThreeVector::ThreeVector (const ThreeVector& original)//original = sum
{ x = original.x;  y = original.y;  z = original.z;
numVectors++;  serialNum = numVectors; cout << " to Copy Constructor, ";
  cout<<" create vector #"<< serialNum<<", this= "<< this <<*this;
}

ThreeVector::~ThreeVector ()
{ cout<<"\n Destructed vector #"<<serialNum<<" containing "<<*this<<" "<<this<<endl;}

const ThreeVector ThreeVector::operator+ (const ThreeVector& rhs) const
{   cout << "\n In operator+, ";
    ThreeVector sum;
    cout << ",  declare sum, &sum = " << &sum << endl;
    sum.x = x + rhs.x;  sum.y = y + rhs.y;  sum.z = z + rhs.z;
    cout << " " << *this <<" + "<<rhs <<" = "<<sum<<"\n\n";
    cout << "Return from operator+..." << endl;
    return sum;
}
ThreeVector& ThreeVector::operator= (const ThreeVector& rhs)

{  cout << " to operator= ";
    x = rhs.x;   y = rhs.y;   z = rhs.z;
    cout << " Assigned "<<*this << this <<" to vector #"<< serialNum <<"."<<endl;
    return *this;
}
ostream& operator<< (ostream& out, const ThreeVector& v)
{   cout << "\n to operator<<, &v= "<< &v;
    out << ", v = (" << v.x << "," << v.y << "," << v.z << ")";
    return out;
}
#endif

Here is the main()
C++:
#include <iostream>
#include "Jtbell.h"
using namespace std;
int main()
{   ThreeVector a(1, 2, 3);
    cout << " &a = " << &a << "\n\n";
    ThreeVector b(4, 5, 6);
    cout << " &b = " << &b << "\n\n";
    ThreeVector c = a + b;
    cout << "\n &c = " << &c << " c = " << c << endl;
    return 0;
}

I ran the program, adding some comments in //green color. It is the same if you run the program, just without the comments.

Basically, when calling operation+, it first create object sum. then do the adding. When "return sum", it calls Copy Constructor to create c and copy sum into c.

When exit the operator+ function, the first call to Destructor is to destroy sum as it's not longer being used. The rest are very straight forward.This is just an analysis from a student here. This has been very educational tracing through the address. Hope I am right.

BTW, this, is what I call FUN! I hate keep going through the overloading operator one after the other and going through those simple programs in the book. This I can get into it, I can't even sleep until 5am last night! It's all your fault!:-p:biggrin:
 

Attachments

  • Option main.docx
    13.2 KB · Views: 151
Last edited:
  • #17
I stepped through the original main from the first post that
C++:
 ThreeVector c; c = a + b;

This is the one that is actually strange. I stepped through it twice to confirm. This is the main
C++:
#include <iostream>
#include "Jtbell.h"
using namespace std;

int main()
{
    ThreeVector a(1, 2, 3);
    cout << " &a = " << &a << "\n\n";
    ThreeVector b(4, 5, 6);
    cout << " &b = " << &b << "\n\n";
    ThreeVector c;
    cout << " &c = " << &c << " c = " << c << "\n\n";
    c = a + b;
    cout << " &c = " << &c << " c = " << c << "\n\n";
    return 0;
}

I print out the output and added comments. It's very strange. Everything works until the line c = a + b. then it creates Vector#5 by copying the content of sum into the Vector#5, then called the operator=() to copy into c again. c already has (5,7,9) from Copy Constructor from sum before. It just repeat again.

Maybe you guys can explain this. Creating Vector#5 and copy into c again is a redundancy. They all happen before exiting operator+(). It is transparent at the end as the value in c is correct, until you really step through it, you won't know the difference.
 

Attachments

  • main.docx
    13.6 KB · Views: 157
Last edited:
  • #18
yungman said:
I stepped through the original main from the first post that
C++:
 ThreeVector c; c = a + b;

This is the one that is actually strange. I stepped through it twice to confirm. This is the main
C++:
#include <iostream>
#include "Jtbell.h"
using namespace std;

int main()
{
    ThreeVector a(1, 2, 3);
    cout << " &a = " << &a << "\n\n";
    ThreeVector b(4, 5, 6);
    cout << " &b = " << &b << "\n\n";
    ThreeVector c;
    cout << " &c = " << &c << " c = " << c << "\n\n";
    c = a + b;
    cout << " &c = " << &c << " c = " << c << "\n\n";
    return 0;
}

I print out the output and added comments. It's very strange. Everything works until the line c = a + b. then it creates Vector#5 by copying the content of sum into the Vector#5, then called the operator=() to copy into c again. c already has (5,7,9) from Copy Constructor from sum before. It just repeat again.

Maybe you guys can explain this. Creating Vector#5 and copy into c again is a redundancy. They all happen before exiting operator+(). It is transparent at the end as the value in c is correct, until you really step through it, you won't know the difference.

I think I know why this program go one more step to copy sum to the Vector#5. If you look at the operator+():
C++:
const ThreeVector ThreeVector::operator+ (const ThreeVector& rhs) const
{   cout << "\n In operator+, ";
    ThreeVector sum;
    cout << ",  declare sum, &sum = " << &sum << endl;
    sum.x = x + rhs.x;  sum.y = y + rhs.y;  sum.z = z + rhs.z;
    cout << " " << *this <<" + "<<rhs <<" = "<<sum<<"\n\n";
    cout << "Return from operator+..." << endl;
    return sum;
}
sum is declare in line 3 , sum is LOCAL inside operator+(). It will be destroy upon exiting the operator+() function. Thereby it has to copy into Vector#5 to pass back to c.

I tried to put ThreeVector sum outside of the function. I even move the operator+() into the class definition to make it an in-line function to put the sum declaration in public, It won't work. I don't know why.
 
  • #19
yungman said:
I tried to put ThreeVector sum outside of the function.
It doesn't make any sense to make sum a global variable. operator+() is defined to return a ThreeVector object, so there should be a variable defined in the body of operator+() for this purpose.
yungman said:
I even move the operator+() into the class definition to make it an in-line function to put the sum declaration in public, It won't work. I don't know why.
It wouldn't matter if you moved the definition of operator+() from the .cpp file to the header, but you can't have a data member that is the same type as the class it belongs to.

Let's focus on these two lines --
C++:
ThreeVector c;
c = a + b;

In the first line, the default constructor is called to create c (ser. # 3). In the following I'm going to abbreviate constructor as ctor, a commonly used abbreviation.
In the next line, operator+() is called. There's a lot going on as a result of this line. The first thing this function does is to call the default ctor to create sum (ser. # 4). operator+() fills in the data members of sum using the values in a and b.

(Note that the second line could have been written as c = a.operator+(b); Here b is the parameter that gets copied to the rhs parameter in @jtbell's code. As @Filip Larsen wrote earlier, copy elision is almost certainly happening, since the copy ctor that jtbell wrote isn't getting called. )

Before returning, operator+() calls the copy ctor to create an unnamed ThreeVector object (ser. # 5).
operator+() then destroys sum (ser. #4) as sum goes out of scope. This is probably the part that you are not getting.

Then operator+() calls operator=() to assign the values in the unnamed ThreeVector object (ser. # 5) to c (ser. # 3). After that, operator+() destroys the unnamed object (ser. # 5) and finally returns to main.
 
  • Like
Likes yungman
  • #20
Just a small repeat of my earlier comment (for yungman's benefit) that adding a copy constructor and assignment operator like it is done in this thread will also remove some of the default move semantics implied by the language, so the behavior discussed in the thread is not always going to happen like that for every class general.

If one wanted to see a more general call trace of the statements in main for a general class one should also add a move constructor and a move assignment operator (to step through in debug and/or with printouts). This would also closer follow what happens when there are no user supplied copy/move constructors and assignment operators, like for instance for a very simple class like the following:

C++:
// C++ 17+
#include <iostream>

struct Vector3 {
  double x{}, y{}, z{};

  Vector3 operator+(const Vector3& rhs) const {
    return {x+rhs.x, y+rhs.y, z+rhs.z};
  }
};

int main()
{
    Vector3 a {1, 2, 3}, b {2, 3, 4};
    Vector3 c = a + b;
    std::cout<<"a + b = " << c.x << " " << c.y << " " << c.z << std::endl;

    return 0;
}

Edit: removed left over partial sentence in the bottom.
 
Last edited:
  • Like
Likes yungman
  • #21
Mark44 said:
It doesn't make any sense to make sum a global variable. operator+() is defined to return a ThreeVector object, so there should be a variable defined in the body of operator+() for this purpose.
It wouldn't matter if you moved the definition of operator+() from the .cpp file to the header, but you can't have a data member that is the same type as the class it belongs to.

Let's focus on these two lines --
C++:
ThreeVector c;
c = a + b;

In the first line, the default constructor is called to create c (ser. # 3). In the following I'm going to abbreviate constructor as ctor, a commonly used abbreviation.
In the next line, operator+() is called. There's a lot going on as a result of this line. The first thing this function does is to call the default ctor to create sum (ser. # 4). operator+() fills in the data members of sum using the values in a and b.

(Note that the second line could have been written as c = a.operator+(b); Here b is the parameter that gets copied to the rhs parameter in @jtbell's code. As @Filip Larsen wrote earlier, copy elision is almost certainly happening, since the copy ctor that jtbell wrote isn't getting called. )

Before returning, operator+() calls the copy ctor to create an unnamed ThreeVector object (ser. # 5).
operator+() then destroys sum (ser. #4) as sum goes out of scope. This is probably the part that you are not getting.

Then operator+() calls operator=() to assign the values in the unnamed ThreeVector object (ser. # 5) to c (ser. # 3). After that, operator+() destroys the unnamed object (ser. # 5) and finally returns to main.
Thanks

I have been working on this, The .docx file in post 17 show the steps agree with what you said here. I just put the WRONG comment in green. It was a long day, I read the address wrong and made the wrong conclusion. I read it through again, the printout agrees with you. Here is the same print out after I remove the comment in green.

I learn a lot here, I did not realize there are so many things going on that unless you step through it, you never realize there are so many extra steps.

I have a feeling there are steps that don't show even when you step through one line at a time. Like you said if there is no Copy Constructor, the compile will put in one. That will be invisible even stepping through. Is there a way to make the invisible steps show?

Thanks
 

Attachments

  • Original main.docx
    13.2 KB · Views: 145
  • #22
Filip Larsen said:
Just a small repeat of my earlier comment (for yungman's benefit) that adding a copy constructor and assignment operator like it is done in this thread will also remove some of the default move semantics implied by the language, so the behavior discussed in the thread is not always going to happen like that for every class general.

If one wanted to see a more general call trace of the statements in main for a general class one should also add a move constructor and a move assignment operator (to step through in debug and/or with printouts). This would also closer follow what happens when there are no user supplied copy/move constructors and assignment operators, like for instance for a very simple class like the following:

C++:
// C++ 17+
#include <iostream>

struct Vector3 {
  double x{}, y{}, z{};

  Vector3 operator+(const Vector3& rhs) const {
    return {x+rhs.x, y+rhs.y, z+rhs.z};
  }
};

int main()
{
    Vector3 a {1, 2, 3}, b {2, 3, 4};
    Vector3 c = a + b;
    std::cout<<"a + b = " << c.x << " " << c.y << " " << c.z << std::endl;

    return 0;
}

there is a lot of default methods and operators that will make this
Yes, I learn this from playing with this thread. I never know before that each step might involve multiple steps that we don't see.

I have modified this program for my own playing now, I adding one more member data char*description so I have the name for each object inside the object. I make it display in each or the function called so I can not only keep track of the address, also keep track of the name.

I am working on passing the name in Copy Constructor and operator=() so the name follows into the newly created object.

I am going to create a new thread if I have more question, I don't want to hijack this thread. Looks like you guys already have this resolved.

This is what I called fun. I was so sick and tired of learning one overload function after the other from the book. Just going through the programs in the book really doesn't give me a good feel. It's playing around with the program like this that I really learn and more interesting. Lately, the word "quit" is getting louder and louder in my head, this thread really got me going again.

Thanks
 
  • #23
yungman said:
I have a feeling there are steps that don't show even when you step through one line at a time. Like you said if there is no Copy Constructor, the compile will put in one. That will be invisible even stepping through. Is there a way to make the invisible steps show?
The only way I can think of is to look at the assembly code that the compiler emits.
 
  • Like
Likes Filip Larsen
  • #24
Mark44 said:
The only way I can think of is to look at the assembly code that the compiler emits.
@yungman making sure you compile for production of course (i.e. with optimisation). With properly written overload operators and constructors (which means among other things that you need to remove any side effects) you will find that a lot of what you see in debug mode disappears and for a small object like this ThreeVector you are just left with a few inlined instructions.

Or you could just read the C++ specification (again bearing in mind that this changed significantly between the C++11 and C++17 specifications, and there may also be "MS inspired" differences in Visual C++), although to understand it you should first prepare yourself with a knowledge of the what, why and wherefore of copy elision: this thread in the developer mailing list indicates some of the thinking, this is a handy reference to some of the terminology.

A lot of dedicated people have been optimising the s**t out of C++ for 30 years, don't imagine that you can even begin to double-guess it with reverse-engineering through the darkened lens of the VS debugger.
 
  • Like
Likes Filip Larsen

What is the purpose of a constructor in C++?

A constructor is a special member function in C++ that is used to initialize objects of a class. It is automatically called when an object is created and is used to set initial values for the data members of the class.

How is a constructor different from a regular member function in C++?

Constructors have the same name as the class and do not have a return type, whereas regular member functions can have any name and must have a return type. Also, constructors are automatically called when an object is created, while regular member functions must be called explicitly.

What is operator overloading in C++?

Operator overloading is a feature in C++ that allows operators to be redefined or extended to work with user-defined data types. This means that operators such as +, -, *, /, etc. can be used with objects of a class, making the code more concise and readable.

How is operator overloading implemented in C++?

In C++, operator overloading is implemented by defining a special member function for the operator in question. This function is called whenever the operator is used with objects of the class. For example, the addition operator + can be overloaded by defining the function "operator+(const MyClass& obj)".

What is the purpose of using a puzzle with C++ constructor and overloaded operator?

This type of puzzle can be used to test a programmer's understanding of constructors and operator overloading in C++. It can also be used to reinforce the concept of object-oriented programming and to improve problem-solving skills.

Similar threads

  • Programming and Computer Science
3
Replies
89
Views
4K
  • Programming and Computer Science
Replies
5
Views
2K
  • Programming and Computer Science
2
Replies
36
Views
2K
  • Programming and Computer Science
3
Replies
89
Views
4K
  • Programming and Computer Science
2
Replies
53
Views
3K
  • Programming and Computer Science
2
Replies
66
Views
4K
  • Programming and Computer Science
Replies
12
Views
1K
  • Programming and Computer Science
Replies
6
Views
8K
  • Programming and Computer Science
2
Replies
35
Views
2K
  • Programming and Computer Science
Replies
23
Views
1K
Back
Top