Improper copy/move behavior: is this a defect?

97 views
Skip to first unread message

Nicol Bolas

unread,
Feb 4, 2016, 10:15:38 PM2/4/16
to ISO C++ Standard - Discussion
You can declare a type which has a defaulted copy constructor, but a deleted move constructor.

struct T
{
  T
() = default;
  T
(const T&) = default;
  T
(T&&) = delete;
};

This is perfectly legal, but it does not do what the user intended. This is because in C++, copying is considered a proper superset of moving. That is, any class which can be copied can be moved as well.

This comes about because if you delete a move constructor, an rvalue reference will bind to `const T&` instead of the deleted `T&&` constructor. Which means this is still legal:

T t{};
T t2
{std::move(t));

The problem with this comes from type composition. Given `T` above, consider `U`:

struct U
{
  T t_
;

  U
() = default;
  U
(const U&) = default;
  U
(U&&) = default;
};

By the rules of implicit move constructor generation, U will be unable to generate a move constructor, since it would have to call the deleted move constructor of T. And therefore, U's move constructor will be deleted. This doesn't make sense to the user, because `is_move_constructible<T>` would be `true`.

However, at the very least, this U is functional. It is copyable and therefore moveable. Just not necessarily efficiently if it contains types that have specialized move constructors.

But what happens when we do this:

struct U
{
  T t_
;
  unique_ptr
<int> up_;

  U
() = default;
  U
(const U&) = default;
  U
(U&&) = default;
};

Well... now what? Since `T` has no move constructor, U's move constructor will be deleted. And since `unique_ptr` has no copy constructor, U's copy constructor will be deleted.

I would call this confusion a defect. It is clear from the standard that any type which is copyable is supposed to also be moveable. Yet a type which is moveable is unable to store move-only members and still be moveable.

Personally, I wish we could just forbid copy-but-no-move types altogether. Make them a compiler error. But that's probably bad for some reason.

The simplest way to resolve this is to make implicit move constructor generation call a subobject's copy constructor if that subobject's move constructor is deleted (obviously unless the copy constructor is also deleted).

Nicol Bolas

unread,
Feb 4, 2016, 10:20:37 PM2/4/16
to ISO C++ Standard - Discussion
On Thursday, February 4, 2016 at 10:15:38 PM UTC-5, Nicol Bolas wrote:
copying is considered a proper superset of moving

Sorry, got that backwards: moving is the proper superset of copying. But I got the description right.

Howard Hinnant

unread,
Feb 4, 2016, 10:30:36 PM2/4/16
to std-dis...@isocpp.org
On Feb 4, 2016, at 10:15 PM, Nicol Bolas <jmck...@gmail.com> wrote:
>
> You can declare a type which has a defaulted copy constructor, but a deleted move constructor.
>
> struct T
> {
> T() = default;
> T(const T&) = default;
> T(T&&) = delete;
> };
>
> This is perfectly legal, but it does not do what the user intended. This is because in C++, copying is considered a proper superset of moving. That is, any class which can be copied can be moved as well.
>
> This comes about because if you delete a move constructor, an rvalue reference will bind to `const T&` instead of the deleted `T&&` constructor. Which means this is still legal:
>
> T t{};
> T t2{std::move(t));

I can find no modern compiler where this is true. Furthermore I believe that the C++14 standard does not support your assertion. std::is_move_constructible<T>{} is false. This is because the deleted move constructor still participates in overload resolution.

Howard

David Krauss

unread,
Feb 4, 2016, 10:33:16 PM2/4/16
to std-dis...@isocpp.org

On 2016–02–05, at 11:15 AM, Nicol Bolas <jmck...@gmail.com> wrote:

Personally, I wish we could just forbid copy-but-no-move types altogether. Make them a compiler error. But that's probably bad for some reason.

Perhaps this could be added alongside (or to) [depr.impldec] §D.1. Say that a future standard reserves the right to implicitly declare a copy constructor/assignment as deleted when the move is.

Nevin Liber

unread,
Feb 4, 2016, 10:39:04 PM2/4/16
to std-dis...@isocpp.org

On 4 February 2016 at 21:15, Nicol Bolas <jmck...@gmail.com> wrote:
Personally, I wish we could just forbid copy-but-no-move types altogether. Make them a compiler error. But that's probably bad for some reason.

Any C++03 class that declared its own copy constructor, copy assignment operator or destructor would stop compiling.

--
 Nevin ":-)" Liber  <mailto:ne...@cplusplusguy.com+1-847-691-1404

Howard Hinnant

unread,
Feb 4, 2016, 10:43:01 PM2/4/16
to std-dis...@isocpp.org
Guideline:

Never explicitly delete a move member. At best this is a no-op. At worst, it doesn’t do what you think it does.

struct NotMovableNorCopyable
{
NotMovableNorCopyable(const NotMovableNorCopyableY) = delete;
NotMovableNorCopyable(NotMovableNorCopyableY&&) = delete; // redundant
};

struct CopyableButNotMovable
{
CopyableButNotMovable(const CopyableButNotMovable) = default;
CopyableButNotMovable(CopyableButNotMovable&&) = delete; // Almost certainly not what intended
};

Howard

David Krauss

unread,
Feb 4, 2016, 10:56:46 PM2/4/16
to std-dis...@isocpp.org

On 2016–02–05, at 11:42 AM, Howard Hinnant <howard....@gmail.com> wrote:

Never explicitly delete a move member.  At best this is a no-op.  At worst, it doesn’t do what you think it does.

Deleting only the move constructor has the same effect as deleting the copy constructor, but being non-movable is more remarkable than being non-copyable.

struct X
{
   X(const X &) = delete;
   // Is X really supposed to be non-movable, or did we just forget X(X &&) = default;?
};

struct Y
{
   Y(Y &&) = delete;
   // OK, not movable. Not copyable goes without saying.
};

Nicol Bolas

unread,
Feb 4, 2016, 11:09:14 PM2/4/16
to ISO C++ Standard - Discussion

Looking at C++14, [class.copy], p11, I see this:

> A defaulted move constructor that is defined as deleted is ignored by overload resolution (13.3, 13.4).

So what I talked about only comes into play when the compiler tried to generate one but couldn't, not when the user said that one doesn't exist.

However, I don't find similar language in C++11. Or at least, it's not as clear. There's a non-normative note:

> When the move constructor is not implicitly declared or explicitly supplied, expressions that otherwise would have invoked the move constructor may instead invoke a copy constructor.

But that's non-normative.

So it seems that copy-but-forbidden-move types do work in C++14, but it's less clear for C++11.

Nicol Bolas

unread,
Feb 4, 2016, 11:21:31 PM2/4/16
to ISO C++ Standard - Discussion

So you're saying the guideline should be "never delete the copy constructor". For copyable types, you default both; for move-only, you default just the move constructor, and for immoble you delete the move constructor.

Should this be suggested for the core C++ guidelines?

Howard Hinnant

unread,
Feb 4, 2016, 11:27:55 PM2/4/16
to std-dis...@isocpp.org
I agree it is a style issue.

It boils down to which idiom is most easily taught and remembered.

Part of my thinking is influenced by my current style of getting special members mentioned right up top of class, and even in a specific order. And when said special members are defaulted, you need easy access to the data members so you can quickly understand what the defaulted special members do.

class Z
{
X x_;
Y y_;
public:
~Z(); // can often be implicitly defaulted. But nothing is more important than this to understanding Z.
Z(); // This must be declared if other constructors are declared unless you do not want it
Z(const Z&); // If deleted you can assume the class is not move-enabled unless stated directly below
Z& operator=(const Z&); // If deleted you can assume the class is not move-enabled unless stated directly below
Z(Z&&); // Won’t exist implicitly if either copy member is declared
Z& operator=(Z&&); // Won’t exist implicitly if either copy member is declared

// other constructors and members ...
};

“Never deleting move members” seems pretty easily teachable and rememberable to me. If you don’t want a type to be copyable or movable, just delete the copy members. If you just want a type to be movable, but not copyable, then only declare the move members.

Howard

Howard Hinnant

unread,
Feb 4, 2016, 11:28:14 PM2/4/16
to std-dis...@isocpp.org
C++11 was borked so badly in this department that it was not shippable.

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1402

As far as I know, no compiler ever shipped strictly conforming to C++11 in this regard.

Howard

Thiago Macieira

unread,
Feb 5, 2016, 12:48:48 AM2/5/16
to std-dis...@isocpp.org
On quinta-feira, 4 de fevereiro de 2016 19:15:38 PST Nicol Bolas wrote:
> struct T
> {
> T() = default;
> T(const T&) = default;
> T(T&&) = delete;
> };
>
> This is perfectly legal, but it does not do what the user intended. This is
> because in C++, copying is considered a proper superset of moving. That is,
> any class which can be copied can be moved as well.

Why would you want a copyable but not movable type? If it can be copied, the
matter is settled.

I would say that it would be more surprising if this suddenly failed:

T t = someFunction();

but this worked:

const T &lifetime_extended = someFunction();
T t = lifetime_extended;

As an analogy, trying to move a file across file systems by the actual move
operation (rename) can fail:

rename("test.cpp", "/var/tmp/test.cpp") = -1 EXDEV (Invalid cross-device link)

But the file can still be moved by way of copy + delete.


--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

Reply all
Reply to author
Forward
0 new messages