4

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

Antoine Morrier
  • 3,930
  • 16
  • 37
  • 1
    It costs a little since it's a pointer and a size, but I'd probably use a getter that returns a `std::string_view` like `struct MyClass { std::string_view getString(){ return m_string; } private: std::string m_string; };` – NathanOliver Dec 15 '20 at 18:40
  • @NathanOliver thanks for your answer, but the `std::string` example is just for example, I want to have a "generic" solution :). I editted the question since it was not clear. – Antoine Morrier Dec 15 '20 at 18:51
  • I haven't had use for rvalue ref-qualified member functions many times myself but in some case some functor-like objects. How are you using it? Wouldn't plain functions work? – Ted Lyngmo Dec 15 '20 at 18:53
  • 1
    Maybe you can write a wrapper with implicit conversion operators, one for rvalue references and one for lvalue references. Then you can use the wrapper as a member, for example `my_moveable m_string;`. – François Andrieux Dec 15 '20 at 18:53
  • 1
    @TedLyngmo I am trying to develop a little library, and so, sometimes, I want to have such behavior. But it is more for curiosity :) – Antoine Morrier Dec 15 '20 at 18:55
  • 1
    The `getString() &&` overload only gets called in situations like `fnReturningMyClass().getString()` and `std::move(myClassObj).getString()`. Do you really need this behaviour? It should have nothing to do with moving one `MyClass` instance to another. It is instead used for stealing the contents of a temporary/end-of-life `MyClass` instance for something else. – alter_igel Dec 15 '20 at 18:56
  • @FrançoisAndrieux Sounds a good idea, I am going to test it later, if you have time, please, write an answer and I would be happy to accept it :) – Antoine Morrier Dec 15 '20 at 18:58
  • I say this because you write `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` - there isn't "another" `MyClass` instance being moved to when you would use a function like `getString() &&`. – alter_igel Dec 15 '20 at 18:59
  • @alterigel about the `getString() &&` overload, I agree with you. However, I mean when you do such things : `auto newInstance = functionThatReturnsMyClass()`, here I want the `std::string` moved and not copied :) – Antoine Morrier Dec 15 '20 at 19:00
  • @AntoineMorrier your class follows the Rule of Zero, (its `m_string` is moved/copied automatically) and this has nothing to do with `getString()` or its overloads – alter_igel Dec 15 '20 at 19:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225999/discussion-between-antoine-morrier-and-alter-igel). – Antoine Morrier Dec 15 '20 at 19:02
  • @FrançoisAndrieux it is not enough easy to use unfortunately :/ – Antoine Morrier Dec 15 '20 at 19:14
  • Related: [Should I make my local variables const or movable?](https://stackoverflow.com/q/61987624/430766) – bitmask Dec 15 '20 at 19:31

4 Answers4

3

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.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • Thanks for your answer :). I think I am ok with the fact that we should avoid getter and setter. However, for `struct` like object, I am happy with getter and setter :) – Antoine Morrier Dec 15 '20 at 19:02
2

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.

facetus
  • 1,091
  • 6
  • 20
  • 1
    I did not understand your solution at first, the video was essential for me to understand. This is indeed a clean and simple solution, though the caveats that the control block needs locking, and that it introduces dynamic allocation are worth mentioning. Though usually these won't matter. I would probably use this instead of my own solution in the future. – François Andrieux Dec 17 '20 at 14:49
  • I would suggest including more details in the answer itself, even if the same information is available through an external link. A large portion of the site's users will not open an external link, and adding the explanation directly in the answer will help this answer reach a wider audience. – François Andrieux Dec 17 '20 at 14:52
  • 1
    I am studying the video, it is very interesting . However, the user can do thing like : `myObject.shared = make_shared()`, so I have to study it before using it :). Thanks a lot for this solution – Antoine Morrier Dec 18 '20 at 19:59
  • I was thinking about including more details to the post, but then Sean's presentation is so fundamental, must watch for everyone, I thought maybe less details will encourage people to watch it. – facetus Dec 19 '20 at 19:46
2

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.

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
  • Don't forget to add rvalue qualified operator, else you can have some issues, but this solution please me :) Thanks – Antoine Morrier Dec 15 '20 at 19:45
  • @facetus You are mistaken, the move assignment operator works just fine. Here is an example where I replace `std::string` with uncopyable type : https://godbolt.org/z/ehxjq9. It is the opposite, a `std::shared_ptr` will require user defined copy construction and assignment to maintain the same behavior. Edit : Fixed link. – François Andrieux Dec 16 '20 at 15:07
  • @facetus I'm not sure what you are talking about, the final draft for C++11 that I have doesn't say that. And the generated assembly for GCC shows `foo::operator=(foo&&)` if used for `foo f; foo g; g = std::move(f);` with C++11 and C++17. And since this is supposedly about a C++11 defect that is already fixed, it would have no relevance to this question (tagged C++20). I have to assume I am not correctly interpreting your comment... – François Andrieux Dec 17 '20 at 01:34
  • @facetus Thanks for the reference, but note that the section you cited was not the same one as this defect report targets. In any case, I still don't see how it relates to this answer as this is not a C++11 question. – François Andrieux Dec 17 '20 at 02:37
  • @facetus Please understand that standard drafts are the best thing most people have access to. As a general rule, for typical discussions they should be assumed accurate unless it is indicated that the relevant section was changed in the final version. – François Andrieux Dec 17 '20 at 02:39
  • Seems the report must be referring a document I can't identify, since the paragraph numbers don't match the titles for any standard I have. – François Andrieux Dec 17 '20 at 02:44
  • @facetus I'm not sure what I said that makes you think I am choosing to stay ignorant. I advise you to re-read my comments with the understanding that the best documentation I have genuinely did not reflect your statements. My confusion about your comment was genuine. Second, my statements are not baseless speculation. Beginning with the assumption that I had made a mistake, I tested and read the doc which both refuted your statement. There was no indication at all that your initial comment was targeting C++11. – François Andrieux Dec 17 '20 at 03:19
-1

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))}{};
François Andrieux
  • 28,148
  • 6
  • 56
  • 87
Red.Wave
  • 2,790
  • 11
  • 17
  • It will always be Undefined Behavior because `m.m_string` is always a `const` object. – François Andrieux Dec 15 '20 at 19:36
  • and it will not solve the issue when you do something like `std::move(myClass).m_string` – Antoine Morrier Dec 15 '20 at 19:44
  • @AntoineMorrier such corners are dangerous; there is a risk of slicing, if the object is only a bit more complex. At least I don`t want to do that. – Red.Wave Dec 16 '20 at 04:04
  • @FrançoisAndrieux I have yet to see a platform that keeps none-static data members in different memory areas. But once the object is a prvalue or an xvalue, I don`t see any hazards in removing the const qualifier; the dtor is about to be called next and it will discard cv qualifiers. RValueness is all about object lifetime. – Red.Wave Dec 16 '20 at 04:11
  • @Red.Wave It doesn't matter, the language forbids it. It is an error to assume that you can predict how the compiler will modify it. At the very least, a compiler is entitled to detect the UB and assume the code will never be reached. See the famous [time traveling UB](https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633). Any code that uses your solution is itself potentially broken by the solution. – François Andrieux Dec 16 '20 at 15:12