Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why do subscript operators C++ often comes in pair?

C++ FAQ is defining a template container Matrix to avoid tricky new delete code. Tutorial says that subscript operators often come in pairs ? Why is it so ?

T&       operator() (unsigned i, unsigned j); 
T const& operator() (unsigned i, unsigned j) const;

Why is it so ?

This is also called : const-overloading.

FAQ gives clues. Do you have additional comments ?

In particular, should mutate() observe certain rules to be used safely on const objects only ?

like image 741
kiriloff Avatar asked Dec 26 '22 07:12

kiriloff


2 Answers

To put it simply, because there are two sides to an assignment: the left side and the right side.

The non-const version: T& operator() (unsigned i, unsigned j); is intended primarily for the left side of an assignment (i.e., to be used as the target of the assignment).

The const version: T const& operator() (unsigned i, unsigned j) const; is intended exclusively for the right side of the assignment.

Note the difference in wording there though: the const version can only be used on the right side of the assignment, whereas the non-const version can be used on either side. If, however, you have a const-qualified object, you can only invoke const-qualified member functions, so in this case it can't be used at all. This is exactly what you (at least normally) want -- it prevents modifying an object you've said shouldn't be modified (by const-qualifying it).

As far as mutate goes, it's typically used only for objects that have some difference between their logical state and their bit-wise state. A common example is a class that does some (usually expensive) calculation lazily. To avoid re-computing the value, it saves the value once it's computed:

class numbers { 
    std::vector<double> values;
    mutable double expensive_value;
    bool valid;
public:
    numbers() : valid(false) {}

    double expensive_computation() const { 
        if (valid) return expensive_value;
        // compute expensive_value here, and set `valid` to true
    }
};

So here, the result from expensive_computation depends purely on the values in values. If you didn't care about speed, you could just re-compute the value every time the user invoked expensive_computation. Calling it repeatedly on a const object will always produce the same result though -- so having called it once, we'll assume it may be called again, and to avoid doing the same expensive computation repeatedly, we just save the value into expensive_value. Then, if the user asks for it again, we just return the value.

In other words, from a logical perspective, the object remains const even when/if we modify expensive_value. The visible state of the object doesn't change. All we've done is allow it to do the const things more quickly.

For this to work correctly, we'd also want to set valid back to false any time the user modifies the contents of values. For example:

void numbers::add_value(double new_val) {
   values.push_back(new_val);
   valid = false;
}

In some cases, we might also want an intermediate level of validity -- we might be able to re-compute expensive_value more quickly by (for example) knowing exactly which numbers have been added to values, rather than just having a Boolean to say whether it's currently valid or not.

I should probably add that C++11 adds some new requirements with respect to both const and mutable. To make a long story short, under most circumstances, you need to ensure that anything that's const and/or mutable is also thread-safe. You might want to watch Herb Sutter's video on this. I do feel obliged to add, however, that I think his conclusion with respect to mutable is probably a little exaggerated (but I'd rather you watched and decided on your own than take my word for it).

like image 196
Jerry Coffin Avatar answered Jan 11 '23 22:01

Jerry Coffin


Call of subscript operator on a T& object, first non-const operator is called. Then, inspect and mutate operations are allowed.

Call of subscript operator on a T const & object, second const operator is called. Then, inspect is allowed, but mutate is not allowed.

Example is here

void f(MyFredList const& a)  ← the MyFredList is const
{
  // Okay to call methods that DON'T change the Fred at a[3]:
  Fred x = a[3];
  a[3].inspect();

  // Error (fortunately!) if you try to change the Fred at a[3]:
  Fred y;
  a[3] = y;       ← Fortunately(!) the compiler catches this error at compile-time
  a[3].mutate();  ← Fortunately(!) the compiler catches this error at compile-time
}

Credits to C++ FAQ, more here.

like image 39
kiriloff Avatar answered Jan 11 '23 23:01

kiriloff