Skip to main content
GameDev.net gamedev.net
🔒 Locked

operator overloads, const, and pointers (C++)

Started by mozie Sep 13, 2010 at 10:58 PM 1 replies 950+ views
Original Post
mozie
mozie
I have been working with some user defined classes of my own, and find that operator overloading is very handy and sometimes required. Specifically the use of the std::set class template, since it requires overloads for custom classes to be able to compare them for sorting and equality. In order to use this type you must define your overloads properly to conform to how the class template was implemented.

The difficulty that I encountered was getting the correct const casting on my overloads. I say “const casting” because I don’t know a better way to put it, and I didn’t want to say “const correctness” because I’m not sure if the way that std::set implements them conforms to the current “const correctness” paradigm (if that’s even the right way to put it).

So this lead me to wonder if the way that I was ‘forced’ into implementing the overloads conforms to const correctness, and that it is the way that I should learn to overload, or if it is unique to its situation somehow and should be treated like a special case.

That eventually led me to being less clear on the function of the ‘operator=’ on the variables that it is used on. Here is what I think is correct for the other operators (I’m not going to discuss the Boolean operations here, though).
Operators:
-, +, *, /, %, <<, >>

These operators work on two variables, resulting in a new variable. We do not want to change either of the two given variables. Thus, the input operator will be const, and the function itself will be const. The return value will be the new variable. Given a mental representation of the process as “C=B+A” where A is the parameter to the operator overload function, B is the “this” of the function in the code (that is to say that we can access B’s members directly), and C is the return value. In this example A and B never change, so we want them to both be const in the code. Since classes can take a lot to copy sometimes, we pass A by reference (and because we defined it as const we don’t have to worry about accidently changing it). Then our function would look something like this for “operator+”:
MyType operator+(const MyType &A) const { return MyType(m_Value + A->m_Value);}


For “operator=” there is only one value to consider, that which is on the right hand side of the equation. There were not very many good results for +”operator=” on google, it seems to strip the equals out and just search for the keyword “operator”. The one place I did get some useful information from basically says that in the “C=B+A” example, “operator=” works as such: “C=B=A”. If it were simply “B=A”, not returning a value, we couldn’t chain assignments as such: “N3 = (N2 = N1)” because if we don’t return our C then “(N2 = N1)” wouldn’t evaluate (it wouldn’t push anything outside the parenthesis) to something passable to N3. Then we are basically setting “this” to the value of the variable ‘A’ and returning a copy of itself (this/B, as C). I guess I demystified my first question just there, because I was unsure of why we had a return value and also self-assignment when it seems like we only had two things to consider (B=A).

Next, my second topic of question related to the same thing. Even though this problem probably just points to a faulty understanding of pointers on my part. Given the syntax:
MyType& operator=(const MyType &A) { … return *this; }


I read “MyType&” as “address of MyType” meaning that the function is returning a pointer to the type MyType. When I see this I consider it to be always used with a new or malloc type of memory structure, that is, it won’t go out of scope when the function returns, and that it will basically be a pointer to an existing object. Accessing the pointer returned by MyType& and changing the value will result in a change to the value that the pointer is pointing to. This does not sit properly with what we want to accomplish with C=(B=A), because if we change C then we would be changing B. This cannot be right, it is not the behavior that we desire at least. Compounding that problem is that when I see “return *this;” I interpret that as “return a copy of this” because we are dereferencing the pointer causing us to treat it as the type and not a pointer to the type. I would expect that if we were returning the address-of-a-copy-of-this then we defeat our first problem of C changing B, but what of the scope of the variable. I would have to assume that the copy of this would be in the scope of the placement of C and not inside the overloaded operator for B.

I am probably just confusing myself more by trying to answer my own questions as I type this, but I just wanted to clearly express my current though process and find out if I am on the right track. Maybe this will help someone with similar difficulties. Also, why shouldn’t I be returning MyType& for the other operators mentioned? I think it didn’t work for me when I tried, but I could have done it wrong
Hodgman
Hodgman
Quote:
I read “MyType&” as “address of MyType” meaning that the function is returning a pointer to the type MyType. When I see this I consider it to be always used with a new or malloc type of memory structure, that is, it won’t go out of scope when the function returns, and that it will basically be a pointer to an existing object. Accessing the pointer returned by MyType& and changing the value will result in a change to the value that the pointer is pointing to. This does not sit properly with what we want to accomplish with C=(B=A), because if we change C then we would be changing B. This cannot be right, it is not the behavior that we desire at least. Compounding that problem is that when I see “return *this;” I interpret that as “return a copy of this” because we are dereferencing the pointer causing us to treat it as the type and not a pointer to the type. I would expect that if we were returning the address-of-a-copy-of-this then we defeat our first problem of C changing B, but what of the scope of the variable. I would have to assume that the copy of this would be in the scope of the placement of C and not inside the overloaded operator for B.
MyType - value
MyType* - pointer to a value
MyType& - reference to a value (acts like a pointer, but looks like a value)

So "MyType& operator=(const MyType &A)
* returns a reference to an object
* takes a read-only reference to an object as input
* no copies or new objects are created. An existing object is passed as input, and an existing object is referenced by the return value.

It's kind of unintuitive at first, but when you put "&" on a value, you get a pointer type (*):
int value = 42;
int* pointer = &value
Also, when you put "*" on a pointer, you get a reference (&):
int& ref1 = *pointer;
You can also create a reference directly from a value:
int& ref2 = value;
All of the above variables point to the original "int value = 42":
assert( value == 42 && *pointer == 42 && ref1 == 42 && ref2 == 42 )

this is a MyType*
*this is a MyType&
&(*this) is a MyType*
*(&(*this)) is a MyType&
So when you "return *this;", you're returning a reference to the existing object that 'owns' the operator= call. This is pretty much the same as returning a pointer - the object is not copied.

When you write "C=(B=A)", you can expand that to "C.operator=( B.operator=(A) )".
When you invoke "B.operator=(A)", then this == &B and *this is a reference to B. So, that function call expands to a reference to B. Then, when you call "C.operator=( ... )", the ... has been replaced with a reference to B.
So, you end up performing "B=A", followed by "C=B".
iMalc
iMalc
Quote:
Original post by mozie
I have been working with some user defined classes of my own, and find that operator overloading is very handy and sometimes required. Specifically the use of the std::set class template, since it requires overloads for custom classes to be able to compare them for sorting and equality. In order to use this type you must define your overloads properly to conform to how the class template was implemented.
That's not quite true. Though it is preferred that you implement operator less-than for a class stored in a set, you could instead write a helper class that implements operator () and use that instead.

Quote:
Original post by mozieThe difficulty that I encountered was getting the correct const casting on my overloads. I say “const casting” because I don’t know a better way to put it, and I didn’t want to say “const correctness” because I’m not sure if the way that std::set implements them conforms to the current “const correctness” paradigm (if that’s even the right way to put it).
Perhaps you should elaborate here. std::set does everything to do with constness exactly as it should, in my opinion.

Quote:
Original post by mozieSo this lead me to wonder if the way that I was ‘forced’ into implementing the overloads conforms to const correctness, and that it is the way that I should learn to overload, or if it is unique to its situation somehow and should be treated like a special case.

That eventually led me to being less clear on the function of the ‘operator=’ on the variables that it is used on. Here is what I think is correct for the other operators (I’m not going to discuss the Boolean operations here, though).
Operators:
-, +, *, /, %, <<, >>

These operators work on two variables, resulting in a new variable. We do not want to change either of the two given variables. Thus, the input operator will be const, and the function itself will be const. The return value will be the new variable. Given a mental representation of the process as “C=B+A” where A is the parameter to the operator overload function, B is the “this” of the function in the code (that is to say that we can access B’s members directly), and C is the return value. In this example A and B never change, so we want them to both be const in the code. Since classes can take a lot to copy sometimes, we pass A by reference (and because we defined it as const we don’t have to worry about accidently changing it). Then our function would look something like this for “operator+”:
MyType operator+(const MyType &A) const { return MyType(m_Value + A->m_Value);}
That is one correct way to write it, but the preferred approach for those operators is to use the two-parameter static or friend version. This has the advantage that either operand then has equal opportunity for being implicitly constructed from a different type, making it easier to things such as (1 + x) as well as (x + 1), with your class.
Also, the basic math operators are usually best implemented in terms of their equivalent left-hand-side-modifying operator:
static MyType operator+(const MyType &A, const MyType &B){    MyType ACopy(A);    ACopy += b;    return ACopy;}
This saves code duplication and yet should result in equally efficient code in a release build, with RVO.

Quote:
MyType& operator=(const MyType &A) { … return *this; }
That is indeed the preferred way to implement the +=, -=, *=, /=, and of course = operator.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.