57

Consider the following example:

#include <iostream>
#include <string>
#include <utility>

template <typename Base> struct Foo : public Base {
    using Base::Base;
};

struct Bar {
    Bar(const Bar&) { }
    Bar(Bar&&) = delete;
};

int main() {
    std::cout << std::is_move_constructible<Bar>::value << std::endl; // NO
    std::cout << std::is_move_constructible<Foo<Bar>>::value << std::endl; // YES. Why?!
}

Why does the compiler generate a move constructor despite the base class being non-move-constructible?

Is that in the standard or is it a compiler bug? Is it possible to "perfectly propagate" move construction from base to derived class?

songyuanyao
  • 147,421
  • 15
  • 261
  • 354
Dmitry
  • 1,123
  • 8
  • 18
  • 5
    Well, you won't have this issue unless you insist on writing classes that are copyable but not movable, and there is no reason to ever do that. – Brian Bi Aug 25 '16 at 07:37
  • The idea behind this question is a possibility to create non-intrusive wrapper classes, which exibit exactly the same external behaviour as their bases. P.S. It was really difficult to pick which answer to accept (I can't accept both, right?) :) – Dmitry Aug 25 '16 at 07:55
  • @Dmitry: No, you can't accept more than one, but you should upvote on any plausible attempt to help you understand the problem. BTW, great question exposing unintuitive behavior! – thokra Aug 25 '16 at 08:41
  • 10
    @Brian: `class FamousPaintingInTheLouvre` ? – einpoklum Aug 25 '16 at 10:50
  • @einpoklum That would ideally be movable but not copyable. – JAB Aug 25 '16 at 17:15
  • 1
    @einpoklum You wouldn't delete the move constructor for that kind of class, you would just let construction from an rvalue use the copy constructor. – Brian Bi Aug 25 '16 at 18:33
  • There is no rule that a move constructor has to actually move anything -- `char *` is move constructible. – David Schwartz Aug 25 '16 at 19:17

2 Answers2

30

Because:

A defaulted move constructor that is defined as deleted is ignored by overload resolution.

([class.copy]/11)

Bar's move constructor is explicitly deleted, so Bar cannot be moved. But Foo<Bar>'s move constructor is implicitly deleted after being implicitly declared as defaulted, due to the fact that the Bar member cannot be moved. Therefore Foo<Bar> can be moved using its copy constructor.

Edit: I also forgot to mention the important fact that an inheriting constructor declaration such as using Base::Base does not inherit default, copy, or move constructors, so that's why Foo<Bar> doesn't have an explicitly deleted move constructor inherited from Bar.

Brian Bi
  • 91,815
  • 8
  • 136
  • 249
  • 2
    Hmm, I don't understand this. The reason seems to be "But Foo's move constructor is implicitly deleted after being implicitly declared as defaulted, due to the fact that the Bar member cannot be moved. Therefore Foo can be moved using its copy constructor." as you described. So, why does it not hold that "Bar's move constructor is explicitly deleted, so Bar can be moved using its copy constructor"? – Johannes Schaub - litb Aug 25 '16 at 07:42
  • 2
    I think "Therefore `Foo` can be moved using its copy constructor." is worded in a very confusing way. It would perhaps be better to reword in a way that doesn't suggest that the object is being *moved from*, only that the (example) call `Foo(std::move(other))` is legal. Or something. – user703016 Aug 25 '16 at 07:45
  • 1
    @AndreasPapadopoulos IMO it might be better to say "`Foo` can be move constructed using its copy ctor". – songyuanyao Aug 25 '16 at 07:47
  • 2
    @Andreas To mere mortals like me, there seems to be no difference between "move constructed" and "moved to". If for a base, the predicate `A` does not hold, it seems very confusing that for a derived class, which needs to do the operation on a `Base` object that `A` asserts to state possibility of, the predicate suddenly yields `true`. It would seem logical to me if this falls within the "short comings" of these predicates that they might miss errors that happen in "non immediate contexts" like side-instantiations etc. But if there's a conceptual reason, I don't yet understand it. – Johannes Schaub - litb Aug 25 '16 at 07:50
  • @JohannesSchaub-litb: The key difference between the two is that `Base` *explicitly* deletes the move ctor (-> compile time error when moving) and `A` *implicitly* deletes it due to what is specified in the clause not fully named by songyuanyao. Since *implicitly* deleted move ctors are not taken into account when trying to move an instance of `A` and because `A::A(A const&)` is then the only ctor left to call when moving, I understand why the predicate is `true` for the derived and `false` for the base. It's frickin' confusing tho - but hey, screw intuition, right? – thokra Aug 25 '16 at 08:31
24

1. The behavior of std::is_move_constructible

This is expected behavior of std::is_move_constructible:

Types without a move constructor, but with a copy constructor that accepts const T& arguments, satisfy std::is_move_constructible.

Which means with a copy constructor it's still possible to construct T from rvalue reference T&&. And Foo<Bar> has an Implicitly-declared copy constructor.

2. The implicitly-declared move constructor of Foo<Bar>

Why does compiler generates move constructor despite base class being non-move-constructible?

In fact, the move constructor of Foo<Bar> is defined as deleted, but note that the deleted implicitly-declared move constructor is ignored by overload resolution.

The implicitly-declared or defaulted move constructor for class T is defined as deleted in any of the following is true:

...
T has direct or virtual base class that cannot be moved (has deleted, inaccessible, or ambiguous move constructors); 
...

The deleted implicitly-declared move constructor is ignored by overload resolution (otherwise it would prevent copy-initialization from rvalue).

3. The different behavior between Bar and Foo<Bar>

Note that the move constructor of Bar is declared as deleted explicitly, and the move constructor of Foo<Bar> is implicitly-declared and defined as deleted. The point is that the deleted implicitly-declared move constructor is ignored by overload resolution, which makes it possible to move construct Foo<Bar> with its copy constructor. But the explicitly deleted move constructor will participate in overload resolution, means when trying to move constructor Bar the deleted move constructor will be selected, then the program is ill-formed.

That's why Foo<Bar> is move constructible but Bar is not.

The standard has an explicit statement about this. $12.8/11 Copying and moving class objects [class.copy]

A defaulted move constructor that is defined as deleted is ignored by overload resolution ([over.match], [over.over]). [ Note: A deleted move constructor would otherwise interfere with initialization from an rvalue which can use the copy constructor instead. — end note ]

songyuanyao
  • 147,421
  • 15
  • 261
  • 354
  • 1
    It is not clear to me where this answer addresses the difference in behaviour between the classes `Bar` and `Foo`. – Marc van Leeuwen Aug 25 '16 at 08:01
  • Hmm... still not too clear to me. Why wouldn't is also be true that **the move constructor for `Bar` that is explicitly deleted is ignored by overload resolution**? I'll guess. Although it is deleted, it still exists, participates in overload resolution, and when it matches, bingo, the program is ill formed. The note http://eel.is/c++draft/dcl.fct.def.delete point 2 appears to say this. The standards committee certainly deserves an award for inventing confusing terminology (here "deleted" for something that continues to exist; something like "forbidden" or "poisoned" would be more suggestive). – Marc van Leeuwen Aug 25 '16 at 09:24
  • @MarcvanLeeuwen The implicitly-declared move constructor also exists even deleted. So I think committee just wants to make the difference between *explicitly deleted* and *implicitly deleted*. If you `deleted` it explicitly, it's supposed that the class doesn't support move semantics at all, including move construction. – songyuanyao Aug 25 '16 at 09:30
  • @MarcvanLeeuwen: Because you **explicitly** state to **NOT** overlook it. Otherwise there wouldn't be a way to **explicitly** express that your type is not movable at all. – thokra Aug 25 '16 at 09:30
  • @songyuanyao: +1 on your answer but please do two more things: a) please add the appropriate clause numbers and b) make it clear that explicit and implicit deletion result in different behavior. – thokra Aug 25 '16 at 09:32
  • @thokra: Suddenly I understand. God explicitly marked the apple in the Garden of Eden as `=delete`, so that Adam and Eve would NOT overlook it. Brilliant. – Marc van Leeuwen Aug 25 '16 at 09:34
  • 2
    @MarcvanLeeuwen: It's confusing as hell ... stuff like this is the curse of C++ in my eyes. – thokra Aug 25 '16 at 09:35
  • @thokra I've tried to add some additional explanations. Clause number, you meant for all the statements? – songyuanyao Aug 25 '16 at 09:39
  • @songyuanyao: Would be nice, even if just for the sake of completeness. – thokra Aug 25 '16 at 09:41
  • "Types without a move constructor, but with a copy constructor that accepts `const T&` arguments, satisfy `std::is_move_constructible`." Shouldn't `Bar` then be move-constructible as it has a copy-constructor? – tkausl Aug 25 '16 at 09:49
  • @songyuanyao: You structured your answer more clearly now, but i was referring to the clause numbers in the C++ standard (e.g. $12.8/11). :) – thokra Aug 25 '16 at 09:58
  • @thokra Only the last paragraph comes from standard, as written in my answer, it's from "$12.8/11 Copying and moving class objects [class.copy]". – songyuanyao Aug 25 '16 at 10:00