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

3

7

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();
}
Conductivity answered 19/8, 2019 at 22:57 Comment(6)
A good solution would be to use differently-named functions for the two cases. std::string takes this route, it was originally designed to support copy-on-write semantics. operator[](n) makes the copy whereas c_str()[n] does not.Accustomed
Another option is to have the function return a proxy object that overloads operators in various ways (e.g. in the Access case the return value might overload operator= to perform the copy)Accustomed
If the non-const version returns a copy, why return it by reference ?Hawger
@SidS It could be int Access() const; but more generally, if something complex is in play here, other than POD, then returning a reference could be better.Conductivity
Maybe you want Copy-on-write which might be tricky to get it right. As many have proposed, the proxy solution would works.Luau
std::as_const could be used at callsideSafeguard
G
7

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())

Gruel answered 19/8, 2019 at 23:40 Comment(0)
M
3

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.

Molding answered 19/8, 2019 at 23:24 Comment(0)
R
0

This might not be a 100% match for the question, but instead of modifying many classes that have this issue, you could create a convenience template function:

template <typename T>
inline T const& cnst(T& a)
{
    return a;
}

This can then be used as such:

Example example;

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

The nice thing about this is that you have full control over when to explicitly use only const access, but if for some reason your object is already const, it will also still work (as a const const& is still a const&).

The downside is of course that you need put this in your code manually, while you would want the compiler to detect it.

This is of course essentially a const_cast, but it's much nicer to use.

Reconstructionism answered 17/9, 2019 at 12:7 Comment(1)
Rather than defining this, use std::as_const.Molding

© 2022 - 2024 — McMap. All rights reserved.