最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Calling member arrow operator -> recursively - Stack Overflow

programmeradmin3浏览0评论

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.

Share Improve this question asked Feb 17 at 20:07 Die4ToastDie4Toast 793 bronze badges New contributor Die4Toast is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 1
  • Your code is a distraction from the real question. I suggest you remove it and the leading paragraphs. Instead, provide code that represents what you are trying to do without a solution. Also, the this->x. and similar in D are unnecessary. Just use x.. The compiler knows that x is the one in the class. – Rud48 Commented Feb 17 at 21:12
Add a comment  | 

2 Answers 2

Reset to default 5

Just 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

发布评论

评论列表(0)

  1. 暂无评论