Search code examples
c++methodsconstantsfunction-declaration

How to declare a const and non-const operator overload in one declaration (templately)?


INTRO

I am writing a class stalker<Obj> that holds inside a variable of type Obj. I want that stalker<Obj> to pretend that it is almost the same as Obj variable (from the user's perspective). However, we hardly know anything about Obj.

So, I try to achieve this by overloading all possible operators (except for literal ones) and I also overload cast operator for the other unexpected behavior. This creates a lot of code:

template <typename Obj>
class stalker {
  Obj obj;

 public:
  operator Obj&() {
    return obj;
  }

  template <typename Arg>
  decltype(auto) operator[](Arg&& arg) {
    return obj[std::forward<Arg>(arg)];
  }

  // and other 20+ overloads...
};

The first problem is that Obj::operator[] can be returning void. I do not want to duplicate my code. Here is how i solved it:

EDIT: there is no need for this (see the discussion below)

// useless invention
template <typename Arg>
decltype(auto) operator[](Arg&& arg) {
  if constexpr (std::is_same_v<void, decltype(std::declval<Obj&>()[std::forward<Arg>(arg)])>) {
    obj[std::forward<Arg>(arg)];
  } else {
    return obj[std::forward<Arg>(arg)];
  }
}

This code already looks heavy (do you know a simpler way?).

PROBLEM

stalker<Obj> can be tagged as const or it has a const type: stalker<const Obj>. Then we need const versions of all our operators.

enter image description here

How can we templately overload a void/non-void and const/non-const operators in one declaration inside a class?

Or is there a better way to achieve stalker<Obj> behaviour identical to Obj?

APPROACHES

CPP Reference has: non-duplicate code of both const & non-const versions (help me to understand this).

Another website provides: https://www.cppstories.com/2020/11/share-code-const-nonconst.html/. However, mutable qualifier or const_cast are not a solution. I hardly understand the part with templates but it seems that in this way we are not pretending as Obj anymore (we need to pass obj in some function).


Solution

  • The first problem is that Obj::operator[] can be returning void.

    It is not a problem, returning void is valid.

    template <typename Arg>
    decltype(auto) operator[](Arg&& arg) { return obj[std::forward<Arg>(arg)]; }
    

    is ok.

    How can we templately overload a void/non-void and const/non-const operators in one declaration inside a class?

    As seen above, void/non-void is no problematic. There are still const/volatile and reference cartesion product overloads though.

    Before C++23, you have to write all of them.

    Since C++23, there is "deducing this" which allows to write just

    template <typename Self, typename Arg>
    decltype(auto) foo(this Self&& self, Arg&& arg) {
        return std::forward<Self>(self).obj[std::forward<Arg>(arg)];
    }
    

    You might SFINAE your method and use same noexcept to mimic more the type:

    template <typename Self, typename Arg>
    auto foo(this Self&& self, Arg&& arg)
    noexcept(noexcept(std::forward<Self>(self).obj[std::forward<Arg>(arg)]))
    -> decltype(std::forward<Self>(self).obj[std::forward<Arg>(arg)])
    {
        return std::forward<Self>(self).obj[std::forward<Arg>(arg)];
    }