Search code examples
c++language-design

C++ Why does the automatic re-application of the arrow (->) operator not apply to pointer-to-pointer types?


This question discusses how the arrow operator -> is automatically re-applied to the return value of an overloaded operator->() until the value returned is a raw pointer, at which point the raw pointer is dereferenced as if with ->. However, the same is not true when one has a pointer-to-pointer type that they wish to dereference to the base value - they must use (*ptr_to_ptr)->foo() instead. It seems to me that usage as ptr_to_ptr->foo() would be unambiguous, even more so than the automatic re-application of -> onto returned values until a raw pointer is returned. So, what was the reasoning behind this decision?

minimal working example:

#include <iostream>

struct Dog {
    void bark() { std::cout << "woof!" << std::endl; }
};

struct DogWalker {
    Dog* dog;
    Dog* operator->() {
        return dog;
    }
};

struct DogOwner {
    DogWalker walker = { new Dog() };
    DogWalker operator->() {
        return walker;
    }
};

void main()
{
    DogOwner owner;
    owner->bark(); // works, prints "woof"

    Dog** ptr_to_ptr = new Dog*;
    *ptr_to_ptr = new Dog;
    (**ptr_to_ptr).bark(); // works
    (*ptr_to_ptr)->bark(); // works
    //ptr_to_ptr->bark(); // ERROR
    //C2227: left of '->bark' must point to class/struct/union/generic type
}

Solution

  • The language adopts much of its semantics from C. The -> operator when applied to a pointer type is only valid if the pointer points to a non-array composite type. Since C doesn't have classes, C++ defined its own semantics for the overloaded -> that made sense for the smart pointer use cases.

    You can achieve the behavior you want with a helper class.

    template <typename T>
    struct Unwrap {
        T *p_;
        Unwrap (T *p = 0) : p_(p) {}
        T * operator -> () const { return p_; }
    };
    
    template <typename T>
    struct Unwrap<T *> {
        T **p_;
        Unwrap (T **p = 0) : p_(p) {}
        Unwrap<T> operator -> () const { return *p_; }
    };
    
    template <typename T>
    Unwrap<T> make_unwrap (T *p) { return p; }
    

    You can then use it like this:

    struct foo {
        void bar () { std::cout << "Hello\n"; }
    };
    
    int main () {
        foo p;
        auto pp = &p;
        auto ppp = &pp;
        auto pppp = make_unwrap(&ppp);
    
        pppp->bar();
    }
    

    Try it online!