operator overloading

In my book the below code for the operator + is written in a way that makes a new temporary Date and returns it without affecting the original object. What if I wanted the + to affect the original object and not make a temporary new Date? How much flexibility do we have in writing these operator codes, could I have written it as such and used it accordingly in main()?


MY CHANGED operator +:
1
2
3
4
5
	Date& operator + (int intToAdd)
	{
		day += intToAdd;
		return *this;
	}



What if I wanted the operator += not just to change the date, but to return the date as well, could have I written it like this and used it properly in main()?

How much flexibility do we have with overloaded operators, or are they fixed as the book shows and should only be used the books way?

I am just finding it VERY hard to believe that it is fixed, given how much flexibility that C++ has. Is it intended to be fixed so that no matter what code you are working, one can reliably use the operators in only one way? Or do you see misused & mistakes in operator overloading in the wild & different versions than expected?

How well memorized do you guys have these overloaded operators, or do you have no trouble treating them as references that you need to go back to look up from time to time?

MY CHANGED operator +=:
1
2
3
4
5
	Date& operator += (int intToAdd)
	{
		day += intToAdd;
		return *this;
	}



BOOK CODE:
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
#include <iostream>
#include <sstream>

using namespace std;

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

	
	Date operator + (int intToAdd)
	{
		Date newDate (month, day + intToAdd, year);
		return newDate;
	}
	
	operator const char* ()
	{
		ostringstream os;
		os << month << "/" << day << "/" << year << endl;
		dateInString = os.str();
		return dateInString.c_str();
		
		
	}
	
	void operator += (int daysToAdd)
	{
		day += daysToAdd;
	}

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

};

int main()	
{
	Date holiday (12, 25, 2022);
	holiday.DisplayDate();

	Date previousHoliday (holiday + 2);
	previousHoliday.DisplayDate();
	holiday.DisplayDate();
	
	holiday += 5;
	holiday.DisplayDate();
	
	return 0;
}
Last edited on
You could do what you describe. The biggest reason why you shouldn't is because it makes the code harder to understand because it breaks common conventions.

The code in the book already takes the liberty of using void as the return type of +=. The built-in and standard library versions of this operator normally returns a reference to the object that you're adding to.

1
2
3
4
5
	Date& operator += (int daysToAdd)
	{
		day += daysToAdd;
		return *this;
	}


The operator overloading syntax is very similar to regular functions. Instead of the function name you write operator@ where @ is the operator that you want to overload. The hardest part for me to remember is whether they can/should be overloaded as members of the class or as non-members. The return type is also something I might want to look up although I probably know it by now, especially for commonly defined operators like the assignment and the function-call operators.
Last edited on
> How much flexibility do we have with overloaded operators?

These are the restrictions: https://en.cppreference.com/w/cpp/language/operators#Restrictions
"the language puts no other constraints on what the overloaded operators do, or on the return type (it does not participate in overload resolution)"


> How well memorized do you guys have these overloaded operators

After a while, we tend to become quite familiar with the commonly used canonical forms.

in general, overloaded operators are expected to behave as similar as possible to the built-in operators: operator+ is expected to add, rather than multiply its arguments, operator= is expected to assign, etc. The related operators are expected to behave similarly (operator+ and operator+= do the same addition-like operation). The return types are limited by the expressions in which the operator is expected to be used: for example, assignment operators return by reference to make it possible to write a = b = c = d, because the built-in operators allow that.
Commonly overloaded operators have the following typical, canonical forms
https://en.cppreference.com/w/cpp/language/operators#Canonical_implementations


For binary arithmetic operators:
Binary operators are typically implemented as non-members to maintain symmetry (for example, when adding a complex number and an integer, if operator+ is a member function of the complex type, then only complex+integer would compile, and not integer+complex). Since for every binary arithmetic operator there exists a corresponding compound assignment operator, canonical forms of binary operators are implemented in terms of their compound assignments

https://en.cppreference.com/w/cpp/language/operators#Binary_arithmetic_operators

For example:

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

struct date
{
    explicit date( int y = 2000, int m = 1, int d = 1 )
        : ymd( std::chrono::year( y ), std::chrono::month( m ), std::chrono::day( d ) )
    {
        if( !ymd.ok() ) throw std::invalid_argument( "bad initialiser for date" ) ;
    }

    int year() const noexcept { return int( ymd.year() ) ; }
    int month() const noexcept { return unsigned( ymd.month() ) ; }
    int day() const noexcept { return unsigned( ymd.day() ) ; }

    auto operator <=> ( const date& that ) const = default ;

    date& operator+= ( int ndays )
    {
        ymd = std::chrono::year_month_day( std::chrono::sys_days( ymd ) + std::chrono::days( ndays ) ) ;
        return *this ;
    }

    std::chrono::year_month_day ymd ;

    // implemented as non-members to maintain symmetry
    // implemented in terms of the corrosponding compound assignment operator
    friend date operator+ ( date dt, int ndays ) { return dt += ndays ; } // note: passed by value
    friend date operator+ ( int ndays, const date& dt ) { return dt + ndays ; }

   friend std::ostream& operator<< ( std::ostream& stm, const date& dt ) { return stm << dt.ymd ; }
    
    /* workaround if the above stream insertion operator is not implemented (eg. with the GNU library) 
    friend std::ostream& operator<< ( std::ostream& stm, const date& dt )
    { return stm << dt.day() << '/' << dt.month() << '/' << dt.year() ; }
    */
};

int main()
{
    date first( 2020, 1, 1 ) ;
    const date second( 2022, 1, 1 ) ;
    while( first < second )
    {
        std::cout << first << '\n' ;
        first += 75 ;
    }
}

http://coliru.stacked-crooked.com/a/5c57469a410c9a65
Note that the operator+ in JLBorges example do not need to be friends; they can be regular non-members, because they do not access private members of 'date', just the date::operator+= and copy contructor.
How much flexibility do we have in writing these operator codes


Really, as much as you want. The operator function gets called with specific parameter(s) (depending whether as member or non-member function). What is done with these and what is returned is down to the programmer (don't forget that you can't overload just on return type).

However, it is convention that these operator functions behave similarly as far as possible as existing usage of these functions - both in respect of operations and the return. But this is not a requirement. eg >> usually means left shift, but when applied to an input stream now means extraction.

But nothing riles programmers more than finding something not working as 'by expected convention'. If this is just for your own use - OK. But if it's for potential usage by others don't antagonise them!
> If this is just for your own use - OK.

Even if it is just for one's own use, writing code that is unintuitive, hard-to-read/understand/maintain is never ok. A good programmer is someone who has cultivated sound programming habits; one who instinctively does not write convoluted code. Moreover, the 'reader' who is flummoxed by a piece of badly written code can be its author, after a few months have passed.
If you want to something custom & weird, I recommend using things like ^, !, and such that make no sense for most objects (unless your object is some sort of bitset like thing). There are a few others as well that would more or less force the user to go read a comment or the code to see what it does.
As an example, I used ! to transpose a matrix, since not(matrix) would have been meaningless in the context of our code there was no risk of confusion.
So that is, to me, the key to it. If the operation makes no sense for the type, then the reader has to look at your documentation or code to see what you did there -- free from assumptions. At that point, you have done your job -- made them check what is going on -- and the code is fine.
In hindsight, maybe they should have put in some unused symbols to overload that carry no assumptions along with them. The problem with that is precedence ... its complex enough already.
Last edited on
Topic archived. No new replies allowed.