Operator overloading

Pages: 123
I am trying to understand operating overloading a bit better and I have 2 questions.

1)Why do we need a return of Date& in this part that is by the book?
1
2
3
4
5
6
	////Prefix++: By the book way
	//Date& operator ++ ()
	//{
	//	++day;
	//	return *this;
	//} 


I can just do it my simpler way below. I don't see a need to send my date1 object in main a reference to itself, it already is itself! But will this be a problem when other programmers look at my code and is it better to just go by the book? What do you guys see in professional programs or programs out there, all sorts of ways or mostly by the book?

1
2
3
4
5
	//Prefix++: My simpler way
	void operator ++ ()
	{
		++day;
	}




2) I am trying to understand how this is working?

1
2
3
4
5
6
7
8
	//Postfix++
	Date operator ++ (int)	
	{
		//Date copyDate(*this);
		Date copyDate(month, day, year);
		++day;
		return copyDate;
	}


From main we have:
date1++.DisplayDate(); //1/4/2000
date1.DisplayDate(); //1/5/2000

So in essence date1++ is changed to equal the "copyDate" from the postfix operator, which is another copy in memory from the original object. ++day gets incremented in the original object and not in "copyDate", which is fine. The ++day NEVER gets incremented in "copyDate" as far as I know.
But then like magic on the next line.
date1.DisplayDate();

All of a sudden "date1" is no longer equal to "copyDate" (where ++day was never incremented), but is equal to the original object as if it reverted back to the original object. So what happened?
Is it because the return of the postfix operator is by value & when the operator goes out of scope the return is destroyed & in so doing "date1" gets reverted back to the original object where ++day took place?

I know that it works, I would love to know how.

Date operator ++ (int)

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
 #include <iostream>
using namespace std;

class Date
{
	int day;
	int month;
	int year;
public:
	Date(int inMonth, int inDay, int inYear) : month(inMonth), day(inDay), year(inYear){}

	void DisplayDate() const
	{
		cout << month<< "/" << day << "/" << year << endl;
	}

	//Prefix++: My simpler way
	void operator ++ ()
	{
		++day;
	}

	////Prefix++: By the book way
	//Date& operator ++ ()
	//{
	//	++day;
	//	return *this;
	//}

	//Postfix++
	Date operator ++ (int)	
	{
		//Date copyDate(*this);
		Date copyDate(month, day, year);
		++day;
		return copyDate;
	}

};

int main()
{
	Date date1(1, 1, 2000);
	date1.DisplayDate();		//1/1/2000
	
	++date1;
	date1.DisplayDate();		//1/2/2000

	++date1;
	date1.DisplayDate();		//1/3/2000

	++date1;
	date1.DisplayDate();		//1/4/2000
	cout << "*****************" << endl;

	date1++.DisplayDate();		//1/4/2000
	date1.DisplayDate();		//1/5/2000
	return 0;
}


Standard Prefix and postfix operators return a value so any implementation should follow this convention - although you don't have to.

postfix returns the value of the original but internally increments. prefix increments and then returns the incremented value. This is one reason prefix returns by ref and postfix by value. Also postfix requires a copy and prefix doesn't. Where copy is expensive ++x will be faster than x++ as x++ requires a copy.
It's an attempt to mimic the behaviour of the built-in operators.

Both ++x and x++ increments x but if you use the value of that expression then ++x will give you the value of x after it got incremented while x++ gives you the value that x had before it was incremented.

Example with the built-in type int:
1
2
3
4
5
6
7
8
int x = 5;
std::cout << "x=" << x << "\n"; // prints x=5

int y = x++;
std::cout << "x=" << x << ", y=" << y << "\n"; // prints x=6, y=5

int z = ++x;
std::cout << "x=" << x << ", z=" << z << "\n"; // prints x=7, z=7 


The same sort of code is expected to work for other types that implement the ++ operator:
1
2
3
4
5
6
7
8
9
10
Date x(1, 1, 2000);
x.DisplayDate(); // 1/1/2000

Date y = x++;
x.DisplayDate(); // 1/2/2000
y.DisplayDate(); // 1/1/2000

Date z = ++x;
x.DisplayDate(); // 1/3/2000
z.DisplayDate(); // 1/3/2000 


To avoid making unnecessary copies of the object (which might be inefficient) the prefix operator simply returns a reference to the object itself. (If you assign the return value to a variable it will still create a copy)

For the postfix operator returning a reference is not an option because it should give you the value that the object had before it was incremented. That is why you need to make a copy, before incrementing, and then return that.
Last edited on
Often you implement post in terms of pre. Eg:

1
2
3
4
5
6
Date operator++(int)
{
    const Date copyDate(*this);
    ++*this;         // call the prefix increment
    return copyDate;
}


So that any type specific code required is only implemented once in pre. In the case of date, this could apply if the day was incremented past the last day of the month.

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
#include <iostream>

class Date {
	// Should be unsigned as a date can't have negative numbers!
	int day;
	int month;
	int year;

public:
	// This should validate provided values
	Date(int inMonth, int inDay, int inYear) : month(inMonth), day(inDay), year(inYear) {}

	void DisplayDate() const {
		std::cout << month << "/" << day << "/" << year << '\n';
	}

	//Prefix++
	Date& operator++() {
		// This should take into account incrementing past number of days in the month
		++day;
		return *this;
	}

	//Postfix++
	Date operator++(int) {
		const Date copyDate(*this);

		++*this;	// Call the prefix operator
		return copyDate;
	}
};

int main() {
	Date date1(1, 1, 2000);
	date1.DisplayDate();		//1/1/2000

	++date1;
	date1.DisplayDate();		//1/2/2000

	++date1;
	date1.DisplayDate();		//1/3/2000

	++date1;
	date1.DisplayDate();		//1/4/2000
	std::cout << "*****************\n";

	date1++.DisplayDate();		//1/4/2000
	date1.DisplayDate();		//1/5/2000
}

Last edited on
1) OK, my simpler way will work for my example but not return anything if I assign it to another object.
Date date2 = ++date1; //oops, no return

2) I see that it tries to mimic the built-in operators, but I am trying to understand precisely how.
date1++.DisplayDate(); //1/4/2000
date1.DisplayDate(); //1/5/2000

"date1" starts out as the first object & we will call it obj1. During the call to "date1++" what happens to "date1"? Does "date1" temporarily become obj2 (the copyDate object) OR does "date1" simply temporarily get replaced with obj2(copyDate object), temporarily for just that line?

It is probably that "date1++" is just a placeholder for the copyDate return, which is returned by value & there is actually an obj2(copyDate object) somewhere temporarily in memory, which then gets destroyed when the operator "Date operator++(int)" goes out of scope?
When the operator goes out of scope copyDate gets destroyed and never to be seen again, and "date1" is still obj1?

Or someone can tell me something unexpected like, "date1" is really a pointer to obj1 in memory & when the ""Date operator++(int)"" is called it points to copyDate and then when it goes out of scope it points back to obj1 in memory again? Or can tell me something wilder than this.
"date1" starts out as the first object & we will call it obj1

date1 is an object. It doesn't refer to an object somewhere else like pointers and references do. It's the same object the whole time until it goes out of scope and is destroyed at the end of the function (although the state/value of the object changes).

During the call to "date1++" what happens to "date1"?

All that happens is that its day member gets incremented.

Does "date1" temporarily become obj2 (the copyDate object) OR does "date1" simply temporarily get replaced with obj2(copyDate object), temporarily for just that line?

Nothing of that. DisplayDate() is called on the return value which is another object.
Last edited on
Thanks.
date1++.DisplayDate(); //1/4/2000
date1.DisplayDate(); //1/5/2000

So it is like saying.
copyDate.DisplayDate() and the ++day gets incremented in the date1 obj.
The next line simply displays the date w/incremented day from the date1 obj.


3) I just did a little test to understand returns a bit better. It looks like regardless of whether you return by value, a reference, or pointer, that the return is temporary & lost when the function goes out of scope. So one either uses it immediately or sets aside another variable or memory location to hold the value, is that right?

Also, just curious as to why the 2 returns that I have commented out won't work that way?

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
#include <iostream>

using namespace std;

//Return by val
int Func1()
	{ 
		int varX = 1; 
		return varX;
	}

//Return by ref	
int& Func2()
	{ 
		int varX = 2; 
		int& refvarX = varX;
		return refvarX;
		
		//return varX;		//??????WHY can't this return to the reference?
	}
	
//Return by pointer
int* Func3()
	{ 
		int varX = 3; 
		int* pvarX = &varX;
		return pvarX;
		
		//return &varX;	//??????WHY can't I return like this?
	}

int main()
{
	//By val
	int varX = Func1();
	cout << Func1() << endl;		//1 Works!
	cout << varX << endl;		   	//1 Works!


	//By ref
	int& refvarX = Func2();
	cout << Func2() << endl;		//2 Works!
	cout << refvarX << endl;		//0  DOES NOT WORK!
	
	int varX2 = Func2();
	cout << varX2 << endl;		   	//2 Works!	
	
	
	//By pointer
	int* pvarX = Func3();
	cout << *(Func3()) << endl;		//3 Works!
	cout << *pvarX << endl;			//0  DOES NOT WORK!
	
	int verX3 = *(Func3());
	pvarX = &verX3;
	cout << *pvarX << endl;			//3 Works!
	
	cout << "*********************" << endl;


//	//Test ref reassign to another ref, which works fine!
//	int num1 = 33;
//	int& ref1 = num1;
//	int& ref2 = ref1;
//	
//	cout << ref1 << endl;			//33 Works!
//	cout << ref2 << endl;			//33 Works!
	

	return 0;
}
/*OUTPUT:
1
1
2
0
2
3
0
3
*********************
*/





Last edited on
So it is like saying.
copyDate.DisplayDate() and the ++day gets incremented in the date1 obj.
The next line simply displays the date w/incremented day from the date1 obj.

Yeah, that's right.


It looks like regardless of whether you return by value, a reference, or pointer, that the return is temporary & lost when the function goes out of scope.

Don't return references or pointers to local variables. Func2() and Func3() is wrong. There is no valid way to access the int that the returned reference/pointer refers to from outside the function because it no longer exists then. It got destroyed when the function ended.


Also, just curious as to why the 2 returns that I have commented out won't work that way?

The compiler is just trying to prevent you from making the mistake that I mentioned above.
Last edited on
Much thanks!
Thanks.
Also, I understand that a true date has to account for days of month & leap years...etc, but this is just a simpler test of operators with less typing.

It feels so much better to hear it from you guys, because even if you research you don't know what you may misinterpret or not understand or may have still missed. When you guys say it collectively it is the final abbreviated word. When I start reading some of these references I usually end up having more questions & frustration than when I started.
Also remember that stuff you read on the internet may not be correct or may be out-of-date. The C++ language changes every 3 years and C++11 brought about a fundamental change (unless you know I'd treat very suspiciously anything published prior to about 2013. The current version is c++23 though not all new additions are yet supported by all compilers. For a list of what features are in which compilers for various versions see https://en.cppreference.com/w/cpp/compiler_support
With searches, sometimes I get quick answers & other times takes a while or never.

I just came across another curious case. I know the fix, but I would like to know why?

4) Curious as to why c++ does not like this line? Why does it not auto detect that we can use the "operator const char*" to a string?
string myStr = ++date1; //NOT WORK????????????

The const char* gets automatically sent to cout & it works.
cout << ++date1 << endl;

A string can be sent a const char* and it works fine.
string myStr2 = pArray;

So, why does C++ send the reference Date& and does not perform the next step to convert to const char*. It should recognize this automatically, that a conversion should happen? Is there any good reason for it not to recognize that line? Shouldn't it?

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
#include <iostream>
#include<sstream>
#include<string>

using namespace std;

class Date
{
	int day;
	int month;
	int year;
	string stringDate;
public:
	Date(int inMonth, int inDay, int inYear) : month(inMonth), day(inDay), year(inYear){}

	void DisplayDate() const
	{
		cout << month<< "/" << day << "/" << year << endl;
	}

	Date& operator ++()		//Prefix ++day
	{
		++day;
		return *this;
	}

	Date& operator --()		//Prefix --day
	{
		--day;
		return *this;
	}

	Date operator ++(int)	//Postfix ++day
	{
		Date copyDate(*this);
		++*this;
		return copyDate;
	}

	Date operator --(int)	//Postfix --day
	{
		Date copyDate(*this);
		--*this;
		return copyDate;
	}

	operator const char* ()
	{
		ostringstream SS;
		SS << month << "/" << day << "/" << year << endl;
		stringDate = SS.str();
		return stringDate.c_str();
	}

};

int main()
{
	Date date1(1, 1, 2024);
	//date1.DisplayDate();
	
	cout << ++date1 << endl;
	cout << date1++ << endl;
	cout << date1 << endl;
	cout << --date1 << endl;
	
	string myStr = ++date1;						//NOT WORK????????????
	//string myStr = (++date1);					//NOT WORK????????????
	//string myStr  = (const char*)++date1;		//works!
	//string myStr (++date1);					//works!
	cout << myStr << endl;


	//Test const char* to string, it works!
	char arrayChar[3] = {'H', 'i', '\0'};
	const char* pArray = arrayChar;
	string myStr2 = pArray;
	cout << myStr2 << endl;				//Hi works!

	return 0;
}

I think it's because it's a two step conversion...

const char* -> std::string -> Date

...which is generally not allowed to happen implicitly.

Implicit conversions can already be confusing and lead to ambiguities so having it happen in multiple steps like that would be even worse. Note that sometimes there might be more than one way to convert, e.g. A -> X -> B or A -> Y -> B.


The way you have written your operator const char* () is wrong because the string data is owned by the local variable SS which is destroyed when the function ends so you're returning a dangling pointer. This is similar to the problem you had earlier.
EDIT: Forget what I said here. I missed that you used the member variable stringDate.


I don't think it makes sense for Date to be implicitly convertible to const char*. To allow outputting with std::cout (and other streams) you would normally overload the << operator instead.

Last edited on
Don't do the conversion to string in the operator cast function. Do this when the value of day changes. Also don't use a char* cast unless you really, really have to. Use string cast instead. Also the 'modern' way to initialise is to use {}. Also as mentioned above you'd overload operator<< for ostream and not have a separate function. 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
84
85
#include <iostream>
#include <sstream>
#include <string>

using namespace std;

class Date {
	int day;
	int month;
	int year;
	string stringDate;

	void toStr() {
		ostringstream SS;

		SS << month << "/" << day << "/" << year << '\n';
		stringDate = SS.str();
	}

public:
	Date(int inMonth, int inDay, int inYear) : month(inMonth), day(inDay), year(inYear) {
		toStr();
	}

	Date& operator ++()	{	//Prefix ++day
		++day;
		toStr();
		return *this;
	}

	Date& operator --()	{	//Prefix --day
		--day;
		toStr();
		return *this;
	}

	Date operator ++(int) {	//Postfix ++day
		const Date copyDate(*this);

		++*this;
		return copyDate;
	}

	Date operator --(int) {	//Postfix --day
		const Date copyDate(*this);

		--*this;
		return copyDate;
	}

	operator string () {
		return stringDate;
	}

	friend ostream& operator<< (ostream& os, const Date& dt) {
		return os << dt.stringDate;
	}
};

int main() {
	Date date1(1, 1, 2024);

	cout << ++date1 << '\n';
	cout << date1++ << '\n';
	cout << date1 << '\n';
	cout << --date1 << '\n';

	// These are OK
	const string myStr1 { ++date1 };				// This is the preferred way using universal initialisation
	const string myStr3 = (string)++date1;
	const string myStr4 { (string)++date1 };
	const string myStr5 (++date1);
	const string myStr6 = { ++date1 };				// Compiles but with warnings. Don't use
	const string myStr7 = (++date1);
	const string myStr8 = ++date1;

	cout << myStr5 << '\n';

	//Test const char* to string, it works!
	const char arrayChar[3] { "HI"};
	const char* const pArray { arrayChar };
	const string myStr2 { pArray };

	cout << myStr2 << '\n';				//Hi works!
}

^^ for dates specifically, a << is nice to have, but a way to format the output is almost mandatory due to the 500 different ways people want dates written. That means you need to define a standard formatter for it, which can be frustrating.
Having a stringify function that just returns a string with parameters, though..

cout << mything.stringify("military"); //prints 20240130
cout << mything.stringify("standard"); //prints Jan 1st 2024
etc.

this fixes two problems:
1) the formatter thing is not beginner friendly and
2) you don't have nonvariables looking like variables in your cout statement:
cout << military << mything; //is military a variable? No, its a code I defined somewhere. good luck chasing that down in a big program.
Last edited on
My feeling too was that it did not like that one extra step, but I felt there is no reason for it. I read about implicit/explicit conversions when sending "MyFunc(55)" to a "void MyFunc(MyClass copyClass)".
So anytime the compiler has to do more than 1 step, it is typically a no & one may have to do some casting or use alternate means. You guys are with more experience, so if you guys say it can be a problem otherwise then I am more willing to just accept that it should be that way.

Isn't the conversion more like:
Date -> const char* -> string

It doesn't mind this line, because it does the 1 step from "Date->const char*" and then the string class method does the 2nd step "const char*->string" internally.
string myStr (++date1); //works

The code is actually a sample from a book that I am just trying to mimic and sometimes I change smaller parts to see what happens and either learn or get frustrated more. I also read a chapter in another newer book that uses the initializers & I try to squeeze them in from time to time to see if they work.

I can see your code and ideas are better suited for a true date application and you make great points to keep in mind when I finally try that too. The stringify method gives a lot of flexibility for the diversity and I did not think about the cout looking like a variable and presenting back tracking issues. Good to be cognizant of this.
I read about implicit/explicit conversions when sending "MyFunc(55)" to a "void MyFunc(MyClass copyClass)"

Usually you don't want this. If MyClass is a numeric class then it might make sense but otherwise it's mostly confusing and it would probably be better to mark the constructor as explicit to prevent the implicit conversion.

https://en.cppreference.com/w/cpp/language/explicit
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c46-by-default-declare-single-argument-constructors-explicit

Isn't the conversion more like:
Date -> const char* -> string

Yeah... I don't know what I was thinking about.

The code is actually a sample from a book that I am just trying to mimic and sometimes I change smaller parts to see what happens and either learn or get frustrated more.

Sounds like a great way to learn. Continue with that! 👍
Last edited on
It doesn't mind this line, because it does the 1 step from "Date->const char*" and then the string class method does the 2nd step "const char*->string" internally.
string myStr (++date1); //works

It works because std::string has a constructor that takes a const char* as argument so this isn't really much different from passing a Date to a function that expects a const char*.

Note that if you change () to = when initializing it will no longer work.
 
string myStr = ++date1; // error 

I think this is because it has traditionally been seen as equivalent to:
 
string myStr(string(++date1));
(ignoring explicit constructors)

So essentially, you would have two steps of implicit conversion again (Date -> const char* -> string) which is not allowed.

Compilers have always been good at optimizing away the extra string copy (since C++17 this is even guaranteed) but that doesn't affect what implicit conversions are allowed in this situation.
Last edited on
Explicit type conversions: https://en.cppreference.com/w/cpp/language/explicit_cast
and custom operators: https://en.cppreference.com/w/cpp/language/cast_operator
seem fun. Does the book get past C-style cast expression into C++-style casts?


Was it Stroustrup that recommended "Do as the ints do"?
Pages: 123