Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get compiler to prefer const method overloading in C++?

I've got a C++ member function of a class with a const and non-const overloading.

Class Example {
public:
  int const & Access() const;
  int       & Access();

  [...]
};

I wish for the const version to be preferred as the performance is way superior in my code, the non-const version causing a copy of the underlying shared object to be created to permit modification.

Right now, if my caller has a non-const Example object, the non-const Access() method is used even if the resultant int is not modified.

Example example;

if ( modify ) {
    example.Access() = 23;    // Need to perform expensive copy here.
} else {
    cout << example.Access(); // No need to copy, read-only access.
}

Is there a way, such as distinguishing lvalue and rvalue uses of the return value, perhaps using perfect forwarding with templates, to create a similar mechanism in C++17 that permits the caller to have one syntax which the compiler only uses the non-const version if the return value is modified?

Another example of where I need this is operator -> () where I have a const and non-const version of the operator. When calling a method that is const I'd like the compiler to prefer the const version of operator -> ().

Class Shared {
  public:
    int Read() const;
    void Write(int value);

    [...]
};

template <typename BaseClass>
class Callback {
public:
  BaseClass const * operator -> () const; // No tracking needed, read-only access.
  BaseClass       * operator -> ();       // Track possible modification.

  [...]
};

typedef Callback<Shared> SharedHandle;

Shared shared;
SharedHandle sharedHandle(&shared);

if ( modify ) {
  sharedHandle->write(23);
} else {
  cout << sharedHandle->Read();
}
like image 395
WilliamKF Avatar asked Aug 19 '19 22:08

WilliamKF


2 Answers

The easiest way to do it would be to make a CAccess member (Like cbegin on stdlib containers):

class Example {
public:
  int const & Access() const;
  int       & Access();
  int const & CAccess() const { return Access(); }
  // No non-const CAccess, so always calls `int const& Access() const`
};

This has the downside that you need to remember to call CAccess if you don't modify it.

You could also return a proxy instead:

class Example;

class AccessProxy {
  Example& e;
  explicit AccessProxy(Example& e_) noexcept : e(e_) {}
  friend class Example;
public:
  operator int const&() const;
  int& operator=(int) const;
};

class Example {
public:
  int const & Access() const;
  AccessProxy Access() {
      return { *this };
  }
private:
  int & ActuallyAccess();
  friend class AccessProxy;
};

inline AccessProxy::operator int const&() const {
    return e.Access();
}

inline int& AccessProxy::operator=(int v) const {
    int& value = e.ActuallyAccess();
    value = v;
    return value;
};

But the downside here is that the type is no longer int&, which might lead to some issues, and only operator= is overloaded.

The second one can easily apply to operators, by making a template class, something like this:

#include <utility>

template<class T, class Class, T&(Class::* GetMutable)(), T const&(Class::* GetImmutable)() const>
class AccessProxy {
  Class& e;
  T& getMutable() const {
      return (e.*GetMutable)();
  }
  const T& getImmutable() const {
      return (e.*GetImmutable)();
  }
public:
  explicit AccessProxy(Class& e_) noexcept : e(e_) {}
  operator T const&() const {
      return getImmutable();
  }
  template<class U>
  decltype(auto) operator=(U&& arg) const {
      return (getMutable() = std::forward<U>(arg));
  }
};

class Example {
public:
  int const & Access() const;
  auto Access() {
      return AccessProxy<int, Example, &Example::ActuallyAccess, &Example::Access>{ *this };
  }
private:
  int & ActuallyAccess();
};

(Though AccessProxy::operator-> would need to be defined too)

and the first method just doesn't work with operator members (Unless you're willing to change sharedHandle->read() into sharedHandle.CGet().read())

like image 54
Artyer Avatar answered Sep 21 '22 21:09

Artyer


You could have the non-const version return a proxy object which determines whether a modification is needed based on its usage.

class Example {
private:
    class AccessProxy {
        friend Example;
    public:
        AccessProxy(AccessProxy const &) = delete;

        operator int const & () const &&
        { return std::as_const(*m_example).Access(); }

        operator int const & operator= (int value) && {
            m_example->assign(value);
            return *this;
        }

        operator int const & operator= (AccessProxy const & rhs) && {
            m_example->assign(rhs);
            return *this;
        }

    private:
        explicit AccessProxy(Example & example) : m_example(&example) {}
        Example * const m_example;
    };

public:
  int const & Access() const;
  AccessProxy Access() { return AccessProxy(*this); }

  // ...

private:
  void assign(int value);
};

This doesn't allow modifying operators directly on the proxy like ++example.Access() or example.Access *= 3, but those could be added as well.

Note this isn't entirely equivalent to the original. Obviously, you can't bind an int& reference to an example.Access() expression. And there could be differences where code that worked before involving a user-defined conversion now fails to compile since it would require two user-defined conversions. The trickiest difference is that in code like

auto && thing = example.Access();

the type of thing is now the hidden type Example::AccessProxy. You can document not to do that, but passing it via perfect forwarding to function templates could get into some dangerous or unexpected behavior. Deleting the copy constructor and making the other public members require an rvalue is an attempt to stop most accidental incorrect uses of the proxy type, but it's not entirely perfect.

like image 45
aschepler Avatar answered Sep 21 '22 21:09

aschepler