Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding an overloaded operator[] example

I am confused with a question that I saw in a c++ test. The code is here:

#include <iostream>
using namespace std;

class Int {
public:
    int v;
    Int(int a) { v = a; }
    Int &operator[](int x) {
        v+=x;
        return *this;
    }
};
ostream &operator<< (ostream &o, Int &a) {
    return o << a.v;
}

int main() {
    Int i = 2;
    cout << i[0] << i[2]; //why does it print 44 ?
    return 0;
}

I was kinda sure that this would print 24 but instead it prints 44. I would really like someone to clarify this. Is it a cumulative evaluation? Also is the << binary infix ?

Thanks in advance

EDIT: in case of not well defined operator overload, could someone give a better implementation of the overloaded operators here so it would print 24?

like image 462
BugShotGG Avatar asked May 22 '15 23:05

BugShotGG


People also ask

Can [] operator be overloaded?

An overloaded operator (except for the function call operator) cannot have default arguments or an ellipsis in the argument list. You must declare the overloaded = , [] , () , and -> operators as nonstatic member functions to ensure that they receive lvalues as their first operands.

What is operator overloading explain with example?

Operator Overloading in C++ This means C++ has the ability to provide the operators with a special meaning for a data type, this ability is known as operator overloading. For example, we can overload an operator '+' in a class like String so that we can concatenate two strings by just using +.

What is overloading in C++ with example?

Function overloading is a feature of object-oriented programming where two or more functions can have the same name but different parameters. When a function name is overloaded with different jobs it is called Function Overloading.

What is the correct way to overload operator?

In case of operator overloading all parameters must be of the different type than the class or struct that declares the operator. Method overloading is used to create several methods with the same name that performs similar tasks on similar data types.


2 Answers

This program has indeterminate behavior: the compiler is not required to evaluate i[0] and i[2] in a left-to-right order (the C++ language gives this freedom to compilers in order to allow for optimizations).

For instance, Clang does it in this instance, while GCC does not.

The order of evaluation is unspecified, so you cannot expect a consistent output, not even when running your program repeatedly on a specific machine.

If you wanted to get consistent output, you could rephrase the above as follows (or in some equivalent way):

cout << i[0];
cout << i[2];

As you can see, GCC no longer outputs 44 in this case.

EDIT:

If, for whatever reason, you really really want the expression cout << i[0] << i[2] to print 24, you will have to modify the definition of the overloaded operators (operator [] and operator <<) significantly, because the language intentionally makes it impossible to tell which subexpression (i[0] or [i2]) gets evaluated first.

The only guarantee you get here is that the result of evaluating i[0] is going to be inserted into cout before the result of evaluating i[2], so your best bet is probably to let operator << perform the modification of Int's data member v, rather than operator [].

However, the delta that should be applied to v is passed as an argument to operator [], and you need some way of forwarding it to operator << together with the original Int object. One possibility is to let operator [] return a data structure that contains the delta as well as a reference to the original object:

class Int;

struct Decorator {
    Decorator(Int& i, int v) : i{i}, v{v} { }
    Int& i;
    int v;
};

class Int {
public:
    int v;
    Int(int a) { v = a; }
    Decorator operator[](int x) {
        return {*this, x}; // This is C++11, equivalent to Decorator(*this, x)
    }
};

Now you just need to rewrite operator << so that it accepts a Decorator object, modifies the referenced Int object by applying the stored delta to the v data member, then prints its value:

ostream &operator<< (ostream &o, Decorator const& d) {
    d.i.v += d.v;
    o << d.i.v;
    return o;
}

Here is a live example.

Disclaimer: As others have mentioned, keep in mind that operator [] and operator << are typically non-mutating operations (more precisely, they do not mutate the state of the object which is indexed and stream-inserted, respectively), so you are highly discouraged from writing code like this unless you're just trying to solve some C++ trivia.

like image 72
Andy Prowl Avatar answered Sep 30 '22 10:09

Andy Prowl


To explain what's going on here, let's make things much simpler: cout<<2*2+1*1;. What happens first, 2*2 or 1*1? One possible answer is 2*2 should happen first, since it's the leftmost thing. But the C++ standard says: who cares?! After all, the result is 5 either way. But sometimes it matters. For instance, if f and g are two functions, and we do f()+g(), then there is no guarantee which gets called first. If f prints a message, but g exits the program, then the message may never be printed. In your case, i[2] got called before i[0], because C++ thinks it doesn't matter. You have two choices:

One option is to change your code so it doesn't matter. Rewrite your [] operator so that it doesn't change the Int, and returns a fresh Int instead. This is probably a good idea anyway, since that will make it consistent with 99% of all the other [] operators on the planet. It also requires less code:

Int &operator[](int x) { return this->v + x;}.

Your other option is to keep your [] the same, and split your printing into two statements:

cout<<i[0]; cout<<i[2];

Some languages actually do guarantee that in 2*2+1*1, the 2*2 is done first. But not C++.

Edit: I was not as clear as I hoped. Let's try more slowly. There are two ways for C++ to evaluate 2*2+1*1.

Method 1: 2*2+1*1 ---> 4+1*1 ---> 4+1 --->5.

Method 2: 2*2+1*1 ---> 2*2+1 ---> 4+1 --->5.

In both cases we get the same answer.

Let's try this again with a different expression: i[0]+i[2].

Method 1: i[0]+i[2] ---> 2+i[2] ---> 2+4 ---> 6.

Method 2: i[0]+i[2] ---> i[0]+4 ---> 4+4 ---> 8.

We got a different answer, because the [] has side effects, so it matters whether we do i[0] or i[2] first. According to C++, these are both valid answers. Now, we are ready to attack your original problem. As you will soon see, it has almost nothing to do with the << operator.

How does C++ deal with cout << i[0] << i[2]? As before, there are two choices.

Method 1: cout << i[0] << i[2] ---> cout << 2 << i[2] ---> cout << 2 << 4.

Method 2: cout << i[0] << i[2] ---> cout << i[0] << 4 ---> cout << 4 << 4.

The first method will print 24 like you expected. But according to C++, method 2 is equally good, and it will print 44 like you saw. Notice that the trouble happens before the << gets called. There is no way to overload << to prevent this, because by the time << is running, the "damage" has already been done.

like image 35
Mark VY Avatar answered Sep 30 '22 10:09

Mark VY