6

I asked a question about move constructors for which I haven't accepted an answer yet because I'm feeling more confused about certain aspects of the question even as I'm starting to get a grip on others. In particular, I've found a surprising case in which both g++ and clang++ generate incorrect move-constructors.

Question summary

  • g++ and clang++ apparently violate the rule that move-constructors are not generated when destructors are explicitly defined; why? Is this a bug, or am I misunderstanding what's going on?
  • For correctness, these (possibly-illegal) move constructors should invalidate RHS pointer members, but they don't. Why not?
  • It appears that the only way to avoid the unwanted behavior is to explicitly define a correct move constructor for every class that uses delete in its destructor. Does the Qt library (version 5.4) do this?

Part 1: Illegally auto-generated constructors?

Consider the following code:

class NoMove
{
  public:
    ~NoMove() {}
};
int main()
{
  std::cout << "NoMove move-constructible? " <<
    std::is_move_constructible<NoMove>::value << std::endl;
}

Compiled with both g++ 4.9.2 and clang++ 3.5.1, this code prints:

NoMove move-constructible? 1

...But since NoMove has an explicitly defined destructor, I would expect that neither a move constructor nor a copy constructor should be auto-generated. Note that the unexpected constructor generation is not due to the fact that the destructor is trivial; I get the same behavior when the destructor delete[]s an array (!!), and I am even able to compile code that requires a valid move constructor (!!!!!). (See example below.) What's going on here? Is it legal to auto-generate a move constructor here, and if so, why?

Part 2: (Possibly illegal) auto-generated constructors causing undefined behavior?

It appears that providing safe move constructors when delete is involved is fairly simple, but I just want to make sure I understand: when a class contains a pointer member and owns the underlying data, is there any case in which it wouldn't be correct and sufficient for the move constructor to invalidate the RHS pointer after setting the destination pointer to the old value?

Consider the following example, which is similar to the NoMove example above and is based on my original question:

class DataType
{
  public:
    DataType()
    {
      val = new int[35];
    }
    ~DataType()
    {
      delete[] val;
    }
  private:
    int* val;
};

class Marshaller
{
  public:
    Marshaller()=default;
    DataType toDataType() &&
    {
      return std::move(data);
    }
  private:
    DataType data;
};

void DoMarshalling()
{
  Marshaller marshaller;
  // ... do some marshalling...
  DataType marshalled_data{std::move(marshaller).toDataType()};
}

This compiles just fine--showing that, yes, DataType has an auto-generated move constructor. And of course, when run, it causes a double-deletion error.

Now, this would be okay, if the auto-generated move constructor invalidated the RHS pointer. So, if it's okay to auto-generate a move constructor here, why isn't that done safely? The move constructor that makes this work is simply:

DataType(DataType&& rhs) :
  val{rhs.val}
{
  rhs.val = nullptr;
}

(Right? Am I missing anything? Should it perhaps be val{std::move(rhs.val)}?)

This seems like it would be a perfectly safe function to auto-generate; the compiler knows that rhs is an r-value because the function prototype says so, and therefore it's entirely acceptable to modify it. So even if DataType's destructor didn't delete[] val, it seems like there wouldn't be any reason not to invalidate rhs in the auto-generated version, except, I suppose, for the fact that this leads to a trivial performance hit.

So if the compiler is auto-generating this method--which, again, it shouldn't, especially since we can just as easily get this exact behavior from standard library code using unique_ptr-- why is it auto-generating it incorrectly?

Part 3: Avoiding this behavior in Qt (especially QByteArray in Qt 5.4)

Finally, a (hopefully) easy question: do Qt 5.4's heap-allocating classes such as QByteArray (which is what I'm actually using as the DataType in my original question) have correctly implemented move constructors, invalidating any moved-from owning pointer(s)?

I wouldn't even bother to ask, because Qt seems pretty solid and I haven't seen any double-deletion errors yet, but given that I was taken off guard by these incorrect compiler-generated move constructors, I'm concerned that it's quite easy to end up with incorrect move constructors in an otherwise-well-implemented library.

Relatedly, what about Qt libraries written before C++11 that don't have explicit move-constructors? If I can accidentally coerce an auto-generated move constructor that behaves erroneously in this case, does anyone know if compiling, say, Qt 3 with a C++11-compliant compiler causes undefined destruction behavior in use-cases like this?

Community
  • 1
  • 1
Kyle Strand
  • 15,941
  • 8
  • 72
  • 167
  • 2
    I'm voting to close this question as off-topic because Dude. One question per question. – Barry Apr 13 '15 at 20:31
  • 3
    @Barry And that makes it...off-topic? Did you actually read the whole thing? The questions are extremely interrelated, tied together, and even cumulative. Simplified, they might be: "1) why do standard compilers generate a move-ctor in this case? 2) why isn't the generated ctor correct? 3) does Qt avoid this error?" – Kyle Strand Apr 13 '15 at 20:34
  • 1
    I did read the whole thing. There are three, separate questions in this question. – Barry Apr 13 '15 at 20:38
  • 1
    I think you'd get better results if you separated that into smaller questions, just because it's unlikely you'll get the novel necessary to answer all of this in one go. – tux3 Apr 13 '15 at 20:45
  • @barry I don't agree, but I sort of see how you might come to that conclusion. I've rewritten some parts of the question to try to present it in a more unified way. – Kyle Strand Apr 13 '15 at 20:46
  • @tux3 Just because it's so long, or because you agree with Barry that the parts themselves constitute genuinely different questions? I can see how part 3 might be better as a separate question, but parts 1 and 2, which constitute most of the text, seem inextricably related: part 2 is just a slightly more complex variation of part 1. I feel like I've seen long questions on this site get good (often long!) answers before. – Kyle Strand Apr 13 '15 at 20:50
  • @KyleStrand I haven't read it well enough to judge whether I agree with Barry, but just from a simple incentive-to-answer PoV, I think you'd be more likely to get good answers with slightly smaller questions. Or maybe give a bounty. But my point is that answering your question is a *lot* of work, and it's already at -1 so it won't get many views and upvotes. But that's just my opinion, if I'm wrong then it's all the better! – tux3 Apr 13 '15 at 20:52
  • For Part 2: What is the definition of a move constructor for basic types? Wait, there isn't one. Would you expect moving from an `int` to set the source to 0? (or a `double` to NaN?) Same with pointers. Pointers don't have a move "constructor", thus they'll use the copy "constructor". The pointer gets copied, and then you go and cause it to be double-deleted. You invoked Undefined Behaviour, not the compiler. You want better behaviour? That's what the smart pointers are for (`std::unique_ptr` perhaps?) – Andre Kostur Apr 13 '15 at 21:48
  • @AndreKostur Honestly, if it were up to me, yes, if "moving" a pointer meant making a copy of the pointer in a new place, I would also require compilers to set the old pointer location to nullptr, with an optimization at the end of function-calls that would allow the old pointer value to be left alone if the memory location will no longer be accessible anyway. But yes, I realize that in the language as it exists, primitive "moves" are just copies. That said, there's a big jump from that to *autogenerated* moves for user-defined types causing undefined behavior. – Kyle Strand Apr 13 '15 at 21:56
  • @KyleStrand I'm not seeing where the autogenerated move constructor for user-defined types causes undefined behaviour. Remember that as soon as you have a user-declared destructor (or a user-declared copy constructor, or user-declared assignment operator(s)), there's no implicit move constructor anymore. Setting a pointer to nullptr for no particular reason is an extra memory write that is unnecessary. It's a naked pointer, there is no "ownership" concept implicitly attached to it. Nothing wrong with 5 naked pointers pointing to the same place. – Andre Kostur Apr 13 '15 at 22:09
  • @AndreKostur Well, yes, you're correct, there was no move constructor in this case; I just *thought* there was, and the question (plus my comment on T.C.'s answer) explains why I was under that impression. Since it was in fact a copy constructor causing the problem, you're right, it would make no sense to set the RHS pointer to null; additionally, it would make no sense for the compiler to auto-generate *any* move constructor in this case (and I completely agree with the decision to deprecate the generation of a copy-constructor when a non-default destructor is defined). – Kyle Strand Apr 13 '15 at 23:57
  • I'm not sure about there being "no particular reason" to set moved-from pointers to null, though, in the hypothetical world where I alone am in charge of the standard. Yes, it would go somewhat against the real-world C/C++ philosophy of "(very very) efficient by default," but I suspect that it would allow developers to safely use the auto-generated functions in more cases than they currently can, and when maximal efficiency is required and it's known to be safe not to invalidate the moved-from pointer, it's trivial to write your own move constructor. – Kyle Strand Apr 14 '15 at 00:02
  • Sure, you could say "it's already trivial to write your own move constructor in cases like this," but how often in the real world is it a good decision to prefer a minor optimization to a reasonably robust correctness guarantee? – Kyle Strand Apr 14 '15 at 00:04
  • @KyleStrand Remember the Rule of Three (Rule of Five for C++11). If you have to implement any one of the copy constructor, copy assignment operator, or destructor (and for C++11 add the move constructor and move assignment operator to make the Rule of Five), you likely will need to implement all of them. In your example, you'd implemented a destructor, but no copy constructor. Having said that, the Rule of 0 is what one should strive for. Using an appropriate smart pointer would have prevented all of your problems (or `std::vector`). – Andre Kostur Apr 14 '15 at 00:44
  • @Andre Technically, I did avoid my problems with a smart pointer, since that's basically what QByteArray is. :) Still, I'm looking forward to seeing that autogeneration deprecation actually come to fruition! – Kyle Strand Apr 14 '15 at 02:46

1 Answers1

7

The problem is that you are confusing is_move_constructible and "has a move constructor". is_move_constructible<T> doesn't test whether T has a move constructor. It tests whether T can be constructed from an rvalue of type T. And const T& can bind to a T rvalue.

What you are seeing is the autogenerated copy constructor T(const T&) doing its work - and failing miserably.

I would expect that neither a move constructor nor a copy constructor should be auto-generated.

Your link talks about the move constructor. It doesn't talk about the copy constructor, which is always implicitly declared if you don't declare it.

Now, if you declared a move operation, the implicitly declared copy constructor would be defined as deleted, but you didn't do that, so it's defined as defaulted and performs a memberwise copy. [class.copy]/p7:

If the class definition does not explicitly declare a copy constructor, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • ....ah. I had read that such auto-generated constructors are deprecated, but I had misremembered that fact as "automatic generation of copy constructors is *more restrictive* than automatic generation of move constructors." I also thought I tested this possibility by explicitly deleting the copy constructor, but I guess I forgot to do so. – Kyle Strand Apr 13 '15 at 22:22
  • It looks like Clang gives a warning for this with "-Wdeprecated," but GCC 4.9.2 does not. Is this an error? – Kyle Strand Apr 16 '15 at 23:11
  • @KyleStrand Not from the standard's perspective. The standard does not require diagnostic messages for using deprecated features. – T.C. Apr 16 '15 at 23:17
  • If the standard required it, why would `-Wdeprecated` be necessary in the first place? Isn't the point of `-Wdeprecated` to give a warning for language deprecations? – Kyle Strand Apr 16 '15 at 23:18