I saw some other QnAs that were written decades ago that implement separate Subscript Operator overloading for reading & writing.
For i.e.: ref 1
class String
{
public:
class Cref;
Cref operator[] (int i);
char operator[] (int i) const;
// ...
};
class String::Cref
{
friend class String;
public:
operator char() { return s.read(i); }
void operator=(char c) { s.write(i, c); }
// ...
};
class X;
class Proxy
{
X* object;
Key key;
public:
Proxy(X* object, Key key) : object(object), key(key) {}
operator V() const { return object->read(key); }
void operator=(V const& v) { object->write(key, v); }
};
class X
{
public:
V read(key) const;
void write(key, V const& v);
Proxy operator[](Key key) { return Proxy(this, key); }
V operator[](Key key) const { return this->read(key); }
};
class CountingProxy
{
public:
CountingProxy<T>& operator=(const T& o)
{
cout << "Is writing\n";
counter_++;
ref_ = o;
return *this;
}
operator T()
{
cout << "Is reading\n";
return ref_;
}
}
class Array
{
public:
CountingProxy<T> operator[] (int index) {
return CountingProxy<T>(changes, data[index]);
}
T operator[] (int index) const {
cout << "Is reading\n";
return data[index];
}
}
... where all 3 cases define the const
variant of subscript operator overloading that doesn't return proxy class(or struct).
Cref operator[] (int i);
char operator[] (int i) const; // <---
Proxy operator[](Key key);
V operator[](Key key) const; // <---
CountingProxy<T> operator[] (int index);
T operator[] (int index) const; // <---
Looking at these, I thought those const overrides are called when not writing.
However, when using my implementation for a simple std::map
wrapper:
#include <iostream>
#include <map>
#include <string>
using namespace std;
struct Proxy
{
string& s;
Proxy(string& s) : s(s) {}
operator string &() const {
cout << "calling string() (read-only)" << endl;
return s;
}
void operator= (const char* val) {
cout << "calling = (write-only)" << endl;
s = val;
}
};
// Method for cout support
ostream& operator<<(ostream& os, const Proxy& self)
{
// In real cases, this can be replaced to `os << self.s;`.
// But to log string() on stdout, making explicit conversion.
string converted_str = self;
os << converted_str;
return os;
}
struct SubscriptOverloadDemo
{
map<string, string> some_map;
Proxy operator[] (const char* key) {
cout << "calling []" << endl;
return Proxy(some_map[key]);
}
const string& operator[] (const char* key) const {
cout << "calling const []" << endl;
return some_map.at(key);
}
};
int main()
{
SubscriptOverloadDemo t;
// Proxy
t["a"] = "asdf";
// const string&?
const string& s = t["a"];
cout << s << endl;
// Proxy
t["a"] = "qwer";
cout << t["a"] << endl;
return 0;
}
... never calls const string& operator[]
and always calls the proxy one.
calling []
calling = (write-only)
calling []
calling string() (read-only)
asdf
calling []
calling = (write-only)
calling []
calling string() (read-only)
qwer
nor that override is ever compiled when looking at the assembly of both MSVC & gcc in Compiler Explorer.
Ultimately I'm trying to create JSON object on my own (for some reason), but this behavior was confusing me, whether it's my implementation fault or is working as intended.
There is no implementation fault. It's that you never have given a chance to call the const T& operator[]
. It will be applied only to the const SubscriptOverloadDemo
object.
For example, try for:
const SubscriptOverloadDemo t2{{ {"a", "asdf"}}};
//^^^^
std::cout << t2["a"] << endl; // It will print "calling const []"
or
cout << std::as_const(t)["a"] << endl; // It will print "calling const []"
// ^^^^^^^^^^^^