The member arrow operator is treated by the compiler in a special way. Since an overloaded arrow operator can have an arbitrary return type, several such operators from different classes may need to be "chained" together in order to obtain a pointer value. This "chaining" process is done automatically/behind the scenes by the compiler. The overloaded class method T operator->() { ... }
can be manually called just like any other method (e.g. return obj.operator->();
), but this just yields a result of type T
. I'd like to know if there's some specific way to manually invoke this overloaded operator->()
method which would result in obtaining the underlying "chained" pointer value instead of the arbitrary result type T
.
The following example has 3 different ways of obtaining the underlying "chained" pointer value that I've thought of, but none of them seem to solve the issue:
struct A {
int data;
int* operator->() { return std::addressof(this->data); }
};
struct B {
A operator->() { return A(); }
};
struct C {
B operator->() { return B(); }
};
template <typename T>
struct D {
T x;
int* operator->() {
// Trying to access the underlying int* pointer
return this->x.operator->(); // (1)
return std::addressof(*this->x); // (2)
return this->x.operator->().operator->().operator->(); // (3)
}
};
int main() {
D<A> da;
D<B> db;
D<C> dc;
}
(1) Calling the operator->()
of the T x
member just like a method. For the D<A>
specialization this will work, however both D<B>
and D<C>
will result in compilation error since the result of calling operator->()
will be B
and C
respectively which are not convertible to int*
.
(2) Using dereference operator and std::addressof
. In the example above this approach doesn't work since none of the classes have operator*()
defined so the compiler throws an error. Even if the derefence operator was defined (iterator classes for instance), the D
class template is supposed to act as a proxy for A
, B
and C
class types, so implementation of D<T>::operator->()
should explicitly use the dependent T::operator->()
. The T::operator*()
and std::addressof
combination seems like a "workaround" which might not even work correctly if the wrapped T::operator->()
has some weird side-effects and T::operator*()
does not.
(3) Manually typing out as many member arrow operator as necessary. This is essentially what compiler does under the hood. That approach is obviously very brittle and it's only going to work for the D<C>
specialization.
For context, I'm currently implementing a type-erased view/range class which can be constructed from any container that can be iterated over. For implementation purposes I also defined a class for type-erased iterators which acts as a proxy to iterators of the underlying collection. The struct D
in the example above represents that type-erased iterator class while the C c
member represents specific/underlying iterator data (e.g. std::vector iterator, some kind of std::ranges::view iterator etc.). Because of type-erasure the return type of D::operator->()
cannot be the same as the one returned by C::operator->()
of the wrapped iterator, since this type also has to be erased.
The member arrow operator is treated by the compiler in a special way. Since an overloaded arrow operator can have an arbitrary return type, several such operators from different classes may need to be "chained" together in order to obtain a pointer value. This "chaining" process is done automatically/behind the scenes by the compiler. The overloaded class method T operator->() { ... }
can be manually called just like any other method (e.g. return obj.operator->();
), but this just yields a result of type T
. I'd like to know if there's some specific way to manually invoke this overloaded operator->()
method which would result in obtaining the underlying "chained" pointer value instead of the arbitrary result type T
.
The following example has 3 different ways of obtaining the underlying "chained" pointer value that I've thought of, but none of them seem to solve the issue:
struct A {
int data;
int* operator->() { return std::addressof(this->data); }
};
struct B {
A operator->() { return A(); }
};
struct C {
B operator->() { return B(); }
};
template <typename T>
struct D {
T x;
int* operator->() {
// Trying to access the underlying int* pointer
return this->x.operator->(); // (1)
return std::addressof(*this->x); // (2)
return this->x.operator->().operator->().operator->(); // (3)
}
};
int main() {
D<A> da;
D<B> db;
D<C> dc;
}
(1) Calling the operator->()
of the T x
member just like a method. For the D<A>
specialization this will work, however both D<B>
and D<C>
will result in compilation error since the result of calling operator->()
will be B
and C
respectively which are not convertible to int*
.
(2) Using dereference operator and std::addressof
. In the example above this approach doesn't work since none of the classes have operator*()
defined so the compiler throws an error. Even if the derefence operator was defined (iterator classes for instance), the D
class template is supposed to act as a proxy for A
, B
and C
class types, so implementation of D<T>::operator->()
should explicitly use the dependent T::operator->()
. The T::operator*()
and std::addressof
combination seems like a "workaround" which might not even work correctly if the wrapped T::operator->()
has some weird side-effects and T::operator*()
does not.
(3) Manually typing out as many member arrow operator as necessary. This is essentially what compiler does under the hood. That approach is obviously very brittle and it's only going to work for the D<C>
specialization.
For context, I'm currently implementing a type-erased view/range class which can be constructed from any container that can be iterated over. For implementation purposes I also defined a class for type-erased iterators which acts as a proxy to iterators of the underlying collection. The struct D
in the example above represents that type-erased iterator class while the C c
member represents specific/underlying iterator data (e.g. std::vector iterator, some kind of std::ranges::view iterator etc.). Because of type-erasure the return type of D::operator->()
cannot be the same as the one returned by C::operator->()
of the wrapped iterator, since this type also has to be erased.
2 Answers
Reset to default 5Just write arrow
functions that mimic what the compiler does for the arrow operator. (None of your attempts mimic the arbitrary depth of what the compiler does.) When you get to a raw pointer, you are done.
template<class T>
T* arrow(T* ptr) {
return ptr;
}
For everything else, invoke operator->
and repeat.
template<class T>
auto* arrow(T&& ptr) {
return arrow(ptr.operator->());
}
(Order matters. The first overload needs to be visible to the second.)
The operator->
of your D
template can then simply return arrow(x);
.
Or if constexpr
version of JaMiT solution
template <typename T>
auto* arrow(T&& t)
{
if constexpr (std::is_pointer_v<std::decay_t<T&&>>) {
return t;
} else {
return arrow(t.operator->());
}
}
Demo
this->x.
and similar in D are unnecessary. Just usex.
. The compiler knows thatx
is the one in the class. – Rud48 Commented Feb 17 at 21:12