2

I'm asking something that looks trivial but I had problems with. Let's assume, for the sake of explanation, a structure like this one:

class MyClass{
    int* m_Number;
public:
    int value() const {return *m_Number;}
    void setValue(int val){*m_Number=val;}
    MyClass() : m_Number(new int(3)){}
    ~MyClass() {if(m_Number) delete m_Number;}
    MyClass(const MyClass& other):m_Number(new int(*other.m_Number)){}
    MyClass& operator=(const MyClass& other){if(m_Number) *m_Number=*other.m_Number; else m_Number=new int(*other.m_Number); return *this;}
    MyClass& operator=(MyClass&& other){std::swap(m_Number,other.m_Number); return *this;}
    MyClass(MyClass&& other){
            //????
    }
}

What should I put in there? My options are:

1)

    MyClass(MyClass&& other)
            :m_Number(other.m_Number)
    {
            other.m_Number=nullptr;
    }

But then the moved from object is not in a valid state. calling value() should return something valid but undetermined while here I just segfault. I could check m_Number in value() and setValue() but you realise it's a huge drag in real code.


2)

    MyClass(MyClass&& other)
            :m_Number(other.m_Number)
    {
            other.m_Number= new int(3);
    }

But a move constructor that can throw is a no go (or at least as I understand it) and it's also a drag of the performance enhancement, in fact this code is equal or worse that the copy constructor.

What do you think?

Did I miss something?

Is there a preferred way to go?

Thanks in advance

Edit: This question received an answer from the convener of the std committee and it fundamentally disagrees with the answers to this post. You can find it in this article https://herbsutter.com/2020/02/17/move-simply/

IlBeldus
  • 1,040
  • 6
  • 14
  • 1
    `other` is supposed to be a temporary. It usually is not possible to call `value()` on it because it disappeared. You cannot expect functions to work on a moved from object besides assigning a new value. The proper fix is to leave the class as is and fix whoever is using a moved-from object inappropriately. – nwp May 09 '17 at 18:41
  • 2
    In #1, why does it matter that the moved-from object is in an invalid state - if you're calling the move constructor, doesn't that mean that the moved-from object is about to be destroyed anyway? – Ben S. May 09 '17 at 18:42
  • 1
    As long as the destructor works in the moved-from state (it does, and you don’t even need the null check because `delete` is safe on a null pointer), that should be good enough. – Daniel H May 09 '17 at 18:42
  • @BenS. no, not necessarily – Jonathan Wakely May 09 '17 at 18:43
  • 1
    Is there really a difference between #2 and not supporting move construction at all? You are essentially implementing another copy constructor. – François Andrieux May 09 '17 at 18:46
  • 1
    Option #1 is fine, you just need to document what operations are valid on the moved-from object. It's perfectly acceptable to say "the only valid operation on a moved-from object is destruction, other operations cause undefined behavior." Then you don't check anything, and if the user invokes `value()` or `setValue()` on a moved-from object, they get UB from the null pointer dereference -- just like you told them. – cdhowie May 09 '17 at 18:48
  • I refer to [this answer](http://stackoverflow.com/a/7028318/4124855) by the guy that put move semantic in the standard in the first place. In the linked video he's even more specific saying that "any function that does not require a precondition can be validly executed by a moved from object" citing std::vector::size as an example hence my doubt here – IlBeldus May 09 '17 at 18:49
  • 4
    @IlBeldus But he's talking about stuff *in the standard library.* Your class is *not* in the standard library -- you can make the rules. Or, to put it another way, specify that the precondition for `value()` and `setValue()` is "this object hasn't been used to move-construct or move-assign another object." Problem solved. Now if they call `value()` or `setValue()` on a moved-from object, they have violated the precondition. – cdhowie May 09 '17 at 18:50
  • 1
    Thanks everyone for your support. I'm a lot more confident in my leave-it-null solution – IlBeldus May 09 '17 at 19:05
  • @cdhowie It’s probably a good idea to at least implement an `isValid` method which just returns whether `m_Number == nullptr`. Then the precondition to all the methods is `isValid()`, and if you might obtain a moved-from object which isn’t immediately destructed (which is possible without UB) you can check to make sure. – Daniel H May 09 '17 at 19:52
  • 1
    @DanielH I don't disagree that one could add such a method, however... *"if you might obtain a moved-from object which isn’t immediately destructed (which is possible without UB)"* -- If you're passing around a moved-from object that says that moved-from objects have no use, that's a pretty clear bug. Of course you can obtain one, but if code does anything with a moved-from object other than allowing it to be destroyed, that's simply misuse of the class. – cdhowie May 09 '17 at 20:01
  • @cdhowie Actually it's totally safe to assign (copy or move assign) to a moved from object in the example above and since my code ends up in a library I don't want to introduce a behaviour the average programmer would not find intuitive by default – IlBeldus May 10 '17 at 09:05

4 Answers4

3

Firstly, there's no reason to use new and delete here, you should be using make_unique<int> to create the object and a unique_ptr<int> to manage it automatically. But that doesn't solve your problem, of what the move constructor can do. There are some other options in addition to the two you suggest:

3) don't give it a move constructor, just leave it as copyable

4) document that calling value or setValue on a moved-from object is not allowed, and leave the moved-from object with a null pointer. Depending where moves happen in your program that might be fine. If moved-from objects are never accessed, everything just works.

4a) As above, but add sanity checks in case it does ever happen:

int value() const {
  assert(m_Number != nullptr);
  return *m_Number;
}

or:

int value() const {
  if (m_Number == nullptr)
    throw std::logic_error("accessed a moved-from object");
  return *m_Number;
}

5) Add checks to setValue to re-initialize the object with a new int if it's currently null, and make value return some default value:

int value() const { return m_Number ? *m_Number : 0; }

void setValue(int val) {
  if (!m_Number)
    m_Number = new int(val);
  else
    *m_Number = val;
}
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
1

You are dereferencing your pointer in the .value() call. You will always segfault in the event that m_Number is invalid.

Your first solution to the move constructor is correct, you should set the 'other' object to a default state. To solve this, you could make your .value() method throw, or return a default value in the event of non-existent resource.

Your destructor already accounts for the null case, so make sure the rest of it accounts for it as well.

TinfoilPancakes
  • 238
  • 1
  • 13
  • 1
    More context: imagine m_Number actually being a [pimpl](https://en.wikipedia.org/wiki/Opaque_pointer) of a class with many methods all accessing it. adding a check in every one of them is 1) expensive in terms of performance 2) quite a lot of boring work – IlBeldus May 09 '17 at 18:56
0
  1. Your first approach is probably the best if modifying MyClass such that m_Number = nullptr is a valid state is reasonable to do (and would be best practice if it were the default state too). I'd argue if having no-heap memory associated with MyClass is not a valid state, you should be allocating it inside of a std::unique_ptr, and passing around a raw pointer to that instead of implementing a move constructor.

  2. If 1. is not an option, this is a reasonable way to go. While there certainly is a performance hit for allocating un-needed memory, it is quite minor, especially considering you are constructing a class as part of this operation (which itself requires memory). If its a small chunk of memory that is needed as in your example, the memory allocator will likely draw it from its own pool without the need of a system call. If it is a large chunk of memory (as I would expect since you're implementing move semantics), then on most modern operating systems it will be a lazy allocation (so it would still be better than a copy constructor).

dlasalle
  • 1,615
  • 3
  • 19
  • 25
  • 1
    Option 2. seems to make the move constructor identical to the copy constructor in performance terms. That performance hit might not be significant, but why bother implementing a move constructor at all if it is no faster than a copy? – Jonathan Wakely May 09 '17 at 23:00
-2

Do not use raw owning pointers, convert your class to use std::unique_ptr, and all your problems will go away.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • 1
    Except that it will still leave an invalid object after a move. – Jonathan Wakely May 09 '17 at 18:42
  • There are often reasons to not use the provided library pointers. This looks like a simplified example; of course there are better options than the simplified example (such as using a literal `3`), but the question is about move semantics in general. – Daniel H May 09 '17 at 18:43
  • I do not understand the problem. Yes, moved from object is in valid, but undefined state. What is the actual question here? – SergeyA May 09 '17 at 18:47
  • 1
    @JonathanWakely Isn't that standard behavior for *any* object that has been moved from? – 0x5453 May 09 '17 at 18:49
  • Turning m_Number into std::unique_ptr and removing the destructor holds the same result. It's kinda not about ownership here. – IlBeldus May 09 '17 at 18:51
  • @0x5453 no, absolutely not. Most objects are still valid after moving from them, e.g. you can still use a `std::string` after moving from it (it's just empty). In the OP's code calling `value()` would be undefined behaviour after a move. – Jonathan Wakely May 09 '17 at 18:53
  • @SergeyA _"moved from object is in valid, but undefined state"_ what does that mean? The OP considers a valid object to be one that you can call `value()` on, so if its pointer is null it's not valid (and it's not "undefined" either, its state is known precisely, it's just not usable as a valid `MyClass`). Using `unique_ptr` is certainly better than manual memory management, but it doesn't answer the OP's question. – Jonathan Wakely May 09 '17 at 18:55