lvalue and rvalue getter, is it possible to remove them?

Viewed 209

Let's say we have this simple class

struct MyClass {
    const std::string &getString() const & {
        return m_string;
    }

    std::string getString() && {
        return std::move(m_string);
    }

  private:
    std::string m_string;
};

As we can see, the m_string acts as a non mutable variable in the sense that we cannot modify it.

This structure also preserve the fact that if we move one instance of MyClass to another, the m_string attribute will be moved as well.

Now, we are going to try to refactor the prior structure :

struct MyClass {
    std::string m_string;
};

Here, we keeps the fact that we can access it or move it, but we lose the "immutability"... So I tried to write it like that :

struct MyClass {
    const std::string m_string;
};

Here we get the immutability thing, however, we lose the potential optimization when we move the object...

So, is it possible to have a behavior similar to the first code, without writing all the getter?

EDIT: the std::string is just for example, but the idea must be usable with all kind of objects

4 Answers

So, is it possible to have a behavior similar to the first code, without writing all the getter?

I can't think of any.

Having said that, the overhead of writing getters and setters for member variables is not such a big burden that I would spend too much time thinking about it.

However, there are some who think that getters and setters of member variables don't add enough protection to a class to even worry about them. If you subscribe to that line of thinking, you can get rid of the getters and setters altogether.

I have used the "no getters and setters" principle for containers of data enough times that I find it natural in many use cases.

The solution is to use std::shared_ptr<const std::string>. A shared pointer to a immutable object has value semantic. Copy-on-write can be achieved using shared_ptr::unique(). See Sean Parent presentation 47:46 https://youtu.be/QGcVXgEVMJg.

You can implement this behavior using a template wrapper type. It seems you want a type that works well with copy and move construction and assignment, but which only provides const access to the wrapped object. All you should need is a wrapper with a forwarding constructor, an implicit conversion operator and dereferencing operators (to force the conversion when implicit conversion doesn't work) :

template<class T>
class immutable
{
public:
    template<class ... A>
    immutable(A&&... args) : member(std::forward<A>(args)...) {}

public:
    operator const T &() const { return member; }
    const T & operator*() const { return member; }
    const T * operator->() const { return &member; }

private:
    T member;
};

This will work well with compiler generated copy and move construction and assignment. The solution is not 100% transparent however. The wrapper will implicitly convert to a reference to the wrapped type, if the context allows it :

#include <string>

struct foo
{
    immutable<std::string> s;
};

void test(const std::string &) {}

int main()
{
    foo f;
    test(f.s); // Converts implicitly
}

But it will need an extra dereference to force the conversion in contexts where implicit conversion will not work :

int main()
{
    foo f;
    //  std::cout << f.s;   // Doesn't work
    std::cout << *(f.s);    // Dereference instead
    //  f.s.size();         // Doesn't work
    f.s->size();            // Dereference instead
}

There was a proposal to add overloading of the . operator, which would allow most cases to work as intended, without a dereferencing. But I'm not sure what the current state of the proposal is.

If only you are willing to declare the copy and defuslt ctor as =default and define the move ctor with const_cast cheat.

MyClass::Myclass()=default;
MyClass::Myclass(Myclass const&)=default;
MyClass::Myclass(Myclass && m)
:   m_string{std::move(const_cast<std::string&>(m.m_string))}{};
Related