I recently discovered an interesting fact about how operator->() behaves
differently from other operator overloads in C++.
Consider this straightforward example:
#include <iostream>
struct A {
void f() { std::cout << "A::f\n"; }
};
int main() {
A* pA = new A;
pA->f();
delete pA;
return 0;
}
This prints “A::f” — no surprises. We create a pointer to an A and use -> to
dereference and call f().
The surprise comes with this:
#include <iostream>
struct A {
void f() { std::cout << "A::f\n"; }
};
struct B {
A* operator->() { return &a; }
A a;
};
struct C {
B operator->() { return b; }
B b;
};
int main() {
C c;
c->f();
return 0;
}
What does c->f() do? The neat thing about operator-> is that it
continually reapplies itself until it reaches a raw pointer. When the raw
pointer is reached, it’s dereferenced and f() is called.
So c->f() expands to (c.operator->())->f(), which gives b->f(), which
expands again until we reach an A*. The output is still “A::f”.
The C++ standard (§13.5.6) describes this:
An expression
x->mis interpreted as(x.operator->())->mfor a class objectxof typeTifT::operator->()exists and if the operator is selected as the best match function by the overload resolution mechanism.
This behavior is what makes smart pointer wrappers like std::unique_ptr and
std::shared_ptr transparent — calling ptr->method() just works, no matter
how many layers of wrapping are involved.