Overloaded Operators

I'm confused over how the overloaded operators decide which argument(s) is/are passed into overloaded operator. I have three examples below which detail three different uses. Hopefully somebody can point me in the right direction.

NOTE: Some of the terminology I use may be incorrect, I am just starting out with C++ so am getting used to it as I go by, and therefore I apologise in advance if my explanations and questions seem confusing, I have tried to explain them as clearly as possible. All of the below code is written purely for examples sake - I do not intend to actually use this code, I have just written it as an example to try and best illustrate where I am confused.

//------------------------------------------
#1): From the example below:

when running in debug mode it is easy to see that 'this' refers to 'e1' and 'other' refers to 'e2', but how is this actually determined? how does the program actually decide which object will be 'this' and which object will be 'other' when passed into the overloaded operator? in this example:

Example e3 = e1 * e2;

is the rule as simple as the argument on the left side of the '*' (e1) becomes 'this' and the argument on the right side of the '*' (e2) is passed into the overloaded operator as the parameter 'const Example& other'?

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
class Example
{
public:
	int x, y;

	Example()
		: x(0), y(0) {}
	
	Example(int x, int y)
		: x(x), y(y) {}

	Example operator*(const Example& other)
	{
		Example new_obj;

		new_obj.x = this->x * other.x; 
		new_obj.y = this->y * other.y; 

		return(new_obj);
	}
};

int main()
{
	Example e1(5, 10);
	Example e2(3, 12);

	Example e3 = e1 * e2;

	std::cout << "e3.x = " << e3.x << std::endl;
	std::cout << "e3.y = " << e3.y << std::endl;
	// Output:
	// e3.x = 15  
	// e3.y = 120 
}


//------------------------------------------
#2): From the example below:

I can again see from running the program in debug mode exactly what is happening:

// this:
Example e4 = e1 * e2 * e3;

// is essentially the same as:
Example e4 = e1 * e2;
e4 = e4 * e3;

but again I am confused as to how the decision is actually made in regards to which argument becomes the parameter in the overloaded operator and why.

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
class Example
{
public:
	int x, y;

	Example()
		: x(0), y(0) {}

	Example(int x, int y)
		: x(x), y(y) {}

	Example operator*(const Example& other)
	{
		Example new_obj;

		new_obj.x = this->x * other.x; 
		new_obj.y = this->y * other.y; 

		return(new_obj);
	}
};

int main()
{
	Example e1(5, 10);
	Example e2(3, 12);
	Example e3(4, 11);

	Example e4 = e1 * e2 * e3;
	
	std::cout << "e4.x = " << e4.x << std::endl;
	std::cout << "e4.y = " << e4.y << std::endl;
	
	// Output:
	// e3.x = 15
	// e3.y = 120 
}


//------------------------------------------
#3): From the example below:

similar to the last in that there are 3 arguments - 'std::cout', 'string' and 'std::endl'. but
here it is the '<<' operator which is overloaded. this example confuses me more than the other two because in the line:

std::cout << string << std::endl;

the second '<<' doesn't seem to actually call the overloaded operator as it did in the previous example when the '*' operator was being overloaded. there are two parameters in the overloaded operator this time, one which takes in an 'ostream' and one which takes in an instance of the 'String' class. from this I can infer that std::cout is being passed to the overloaded operator as the 'stream' parameter whilst the 'string' object is passed in by reference. so it is fairly obvious that the operation being performed in the overloaded operator is essentially:

std::cout << string.m_Buffer;

but why does the second '<<' in the line:

std::cout << string << std::endl;

not call the overloaded operator again?

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
class String
{
	char* m_Buffer;
	unsigned int m_Size;
public:
	String(const char* string)
		
	{
		m_Size = strlen(string);
		m_Buffer = new char[m_Size]; 
		memcpy(m_Buffer,string,m_Size);
	}

	~String()
	{

	}

	friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

std::ostream& operator<<(std::ostream& stream, const String& string)
{
	stream << string.m_Buffer;
	return(stream);
}

int main()
{
	String string("Ryan");
	std::cout << string << std::endl;
}

//------------------------------------------
Last edited on
// is essentially the same as:
Example e4 = e1 * e2;
e4 = e4 * e3;

No, that is not what is happening.
When evaluating:
 
	Example e4 = e1 * e2 * e3;

The compiler generates a temporary Example object into which the result of e1 * e2 is stored.
The temporary object object is then multiplied by e3. Only then is the result stored in e4.

but why does the second '<<' in the line:

std::cout << string << std::endl;

not call the overloaded operator again?

Because std::endl is not a String, therefore String's overload of the << operator is not called.



#1
is the rule as simple as the argument on the left side of the '*' (e1) becomes 'this' and the argument on the right side of the '*' (e2) is passed into the overloaded operator as the parameter 'const Example& other'?

Yes, it's that simple. ;)


#2
I am confused as to how the decision is actually made in regards to which argument becomes the parameter in the overloaded operator and why.

The * operator is left-to-right associative so e1 * e2 * e3 is evaluated as (e1 * e2) * e3. Another way to write this expression is e3.operator*(e1.operator*(e2)) e1.operator*(e2).operator*(e3).

https://en.cppreference.com/w/cpp/language/operator_precedence


#3
there are two parameters in the overloaded operator this time, one which takes in an 'ostream' and one which takes in an instance of the 'String' class.

That is because the operator is being defined outside the class. You could have done the same with operator* if you wanted. With operator<< it is necessary to define it outside the class because you want the first argument to be of type std::ostream& and you have no way of defining it inside the std::ostream class. Defining it inside your string class would force you to pass the string as first argument but that is not what you want.

why does the second '<<' in the line:
std::cout << string << std::endl;
not call the overloaded operator again?

. std::cout << string will return a reference to std::cout so the second << will be called with std::cout as the first argument and std::endl as the second argument (same as std::cout << std::endl;). These argument types does not match your overload, instead it calls another overload that does match.
Last edited on
Okay I have slightly rejigged my example for the purpose of illustration.

So would it be correct to say that the overloaded operator will only be called if the type and order of the arguments match that of the operators parameters?

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
class Example
{
public:
	int x, y;

	Example()
		: x(0), y(0) {}

	Example(int x, int y)
		: x(x), y(y) {}

	friend Example operator*(Example, const Example&);
};

Example operator*(Example left, const Example& right)
{
	Example new_obj;

	new_obj.x = left.x * right.x; 
	new_obj.y = left.y * right.y; 

	return(new_obj);
}

int main()
{
	Example e1(5, 10);
	Example e2(3, 12);
	Example e3(4, 11);

	Example e4 = (e1 * e2) * e3;
	
	std::cout << "e4.x = " << e4.x << std::endl;
	std::cout << "e4.y = " << e4.y << std::endl;
	
	// Output:
	// e4.x = 60
	// e4.y = 1320 
}


In the line:

 
Example e4 = (e1 * e2) * e3;


On the first call of the overloaded operator, 'e1' matches the type 'Example' (first parameter of the overloaded operator) and 'e2' matches the type 'const Example&' (second parameter of the overloaded operator), so the operator is called.

On the second call of the overloaded operator, the temporary object holding the product of 'e1 * e2' from the previous call, matches the type 'Example' (first parameter of the overloaded operator) and 'e3' matches the type 'const Example&' (second parameter of the overloaded operator), so the operator is called.

If I had done this instead:

1
2
3
4
5
float F = 1.5F;
Example e1(5, 10);
Example e2(3, 12);

Example e4 = (e1 * e2) * F;


Then I would get an error because 'F' does not match the type of the second parameter in the overloaded operator, so the second call would fail.

---------------------------------------------------------------------------------------------------------------------------------

And presumably this is a similar situation as in my third example where:

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
class String
{
	char* m_Buffer;
	unsigned int m_Size;
public:
	String(const char* string)
		
	{
		m_Size = strlen(string);
		m_Buffer = new char[m_Size]; 
		memcpy(m_Buffer,string,m_Size);
	}

	friend std::ostream& operator<<(std::ostream& stream, const String& string);
};

std::ostream& operator<<(std::ostream& stream, const String& string)
{
	stream << string.m_Buffer;
	return(stream);
}

int main()
{
	String string("Ryan"); 
	std::cout << string << std::endl;
	
	std::cin.get();
}


The first '<<' calls the overloaded operator because the first argument - 'std::cout' matches the type of the first parameter in the overloaded operator (std::ostream&), whilst the second argument 'string' matches the type of the second parameter in the overloaded operator (const String&).

The line:

 
stream << string.m_Buffer;


Is therefore executed and "Ryan" is printed to the console.

But on the second '<<'; the first argument (to the left of the 2nd '<<') - 'string' does not match the type of the first parameter in the overloaded operator (std::ostream&), and the second argument (to the right of the 2nd '<<') - 'std::endl' does not match the type of the second parameter in the overloaded operator (const String&), so therefore the overloaded operator is not called, and the 2nd '<<' is executed normally, as an output operator, which outputs 'std::endl' normally.

Am I now on the right tracks?
Last edited on
So would it be correct to say that the overloaded operator will only be called if the type and order of the arguments match that of the operators parameters?

Yes.

On the first call of the overloaded operator, 'e1' matches the type 'Example' (first parameter of the overloaded operator) and 'e2' matches the type 'const Example&' (second parameter of the overloaded operator), so the operator is called.

On the second call of the overloaded operator, the temporary object holding the product of 'e1 * e2' from the previous call, matches the type 'Example' (first parameter of the overloaded operator) and 'e3' matches the type 'const Example&' (second parameter of the overloaded operator), so the operator is called.

Correct.

If I had done this instead:
1
2
3
4
float F = 1.5F;
Example e1(5, 10);
Example e2(3, 12);
Example e4 = (e1 * e2) * F;

Then I would get an error because 'F' does not match the type of the second parameter in the overloaded operator, so the second call would fail.

Correct. You would get a compile error.

But on the second '<<'; the first argument (to the left of the 2nd '<<') - 'string' does not match the type of the first parameter in the overloaded operator (std::ostream&), and the second argument (to the right of the 2nd '<<') - 'std::endl' does not match the type of the second parameter in the overloaded operator (const String&), so therefore the overloaded operator is not called, and the 2nd '<<' is executed normally, as an output operator, which outputs 'std::endl' normally.

Am I now on the right tracks?

Yes.


But on the second '<<'; the first argument (to the left of the 2nd '<<') - 'string' does not match the type of the first parameter in the overloaded operator (std::ostream&), and the second argument (to the right of the 2nd '<<') - 'std::endl' does not match the type of the second parameter in the overloaded operator (const String&), so therefore the overloaded operator is not called, and the 2nd '<<' is executed normally, as an output operator, which outputs 'std::endl' normally.


string is not the first argument of the second <<.

The expression is evaluated like this:
 
(std::cout << string) << std::endl;

(std::cout << string) returns a reference to std::cout (the first argument) which becomes the first argument to the second <<.

This is why we always return a reference to the stream when overloading the << operator. If we didn't, we wouldn't be able to chain them and would have to write like this:
1
2
std::cout << string;
std::cout << std::endl;
Last edited on
This is why we always return a reference to the stream when overloading the << operator. If we didn't, we wouldn't be able to chain them and would have to write like this:
1
2
std::cout << string;
std::cout << std::endl;

So in the 2nd line of the above code snippet, the 'std::cout' is essentially the return value of 'stream'. And then in the line:

 
std::cout << string << std::endl;

On the second '<<', the first argument is the value returned from the overloaded operator ('stream') and the second argument is 'std::endl', which does not match the type of the second parameter in the overloaded operator (const String&), so therefore the operator isn't called for a second time, and 'std::endl' is just executed normally, as an output operator, which outputs 'std::endl' normally. Is this correct?
Last edited on
I think you've got the right idea.

Note that there are multiple overloads of << in the std namespace. One for char, one for int, one for strings, etc. There is no "normal" one. std::endl is actually a function that takes an output stream as argument, so you can use it like this: std::endl(std::cout). The reason you can use it with << (which is how it is meant to be used) is because there is an overload of << that takes such a function as its second argument and passes the stream as argument to the function.
Last edited on
Peter87: Okay, I think I understand this now. Just to make absolutely sure I have this right, if I were to split the line:

 
std::cout << string << std::endl;

Into two parts, then it would read like this:

1
2
3
std::cout << string; // this happens first
return_value << std::endl; // then this second
// return value is 'stream' (std::cout) 

Note: Obviously the code wouldn't actually be written like that over two lines but I have split the operations up just for the purpose of illustration.

Is this effectively how the that line is evaluated?
Last edited on
Yes. That is right.

1
2
3
// To make your code compile:
std::ostream& return_value = (std::cout << string);
return_value << std::endl;
Last edited on
Peter87: Great. Thanks a lot for taking the time to make sure I had a decent grasp of this :)
Topic archived. No new replies allowed.