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();
}
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 operator
s, 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()
)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With