How to get if a type is truly move constructible

2019-04-09 08:29发布

问题:

Take for example this code:

#include <type_traits>
#include <iostream>

struct Foo
{
    Foo() = default;
    Foo(Foo&&) = delete;
    Foo(const Foo&) noexcept
    {
        std::cout << "copy!" << std::endl;
    };
};

struct Bar : Foo {};

static_assert(!std::is_move_constructible_v<Foo>, "Foo shouldn't be move constructible");
// This would error if uncommented
//static_assert(!std::is_move_constructible_v<Bar>, "Bar shouldn't be move constructible");

int main()
{
    Bar bar {};
    Bar barTwo { std::move(bar) };
    // prints "copy!"
}

Because Bar is derived from Foo, it doesn't have a move constructor. It is still constructible by using the copy constructor. I learned why it chooses the copy constructor from another answer:

if y is of type S, then std::move(y), of type S&&, is reference compatible with type S&. Thus S x(std::move(y)) is perfectly valid and call the copy constructor S::S(const S&).

—Lærne, Understanding std::is_move_constructible

So I understand why a rvalue "downgrades" from moving to a lvalue copying, and thus why std::is_move_constructible returns true. However, is there a way to detect if a type is truly move constructible excluding the copy constructor?

回答1:

There are claims that presence of move constructor can't be detected and on surface they seem to be correct -- the way && binds to const& makes it impossible to tell which constructors are present in class' interface.

Then it occurred to me -- move semantic in C++ isn't a separate semantic... It is an "alias" to a copy semantic, another "interface" that class implementer can "intercept" and provide alternative implementation. So the question "can we detect a presence of move ctor?" can be reformulated as "can we detect a presence of two copy interfaces?". Turns out we can achieve that by (ab)using overloading -- it fails to compile when there are two equally viable ways to construct an object and this fact can be detected with SFINAE.

30 lines of code are worth a thousand words:

#include <type_traits>
#include <utility>
#include <cstdio>

using namespace std;

struct S
{
    ~S();
    //S(S const&){}
    //S(S const&) = delete;
    //S(S&&) {}
    //S(S&&) = delete;
};

template<class P>
struct M
{
    operator P const&();
    operator P&&();
};

constexpr bool has_cctor = is_copy_constructible_v<S>;
constexpr bool has_mctor = is_move_constructible_v<S> && !is_constructible_v<S, M<S>>;

int main()
{
    printf("has_cctor = %d\n", has_cctor);
    printf("has_mctor = %d\n", has_mctor);
}

Notes:

  • you probably should be able to confuse this logic with additional const/volatile overloads, so some additional work may be required here

  • doubt this magic works well with private/protected constructors -- another area to look at

  • doesn't seem to work on MSVC (as is tradition)



回答2:

How to find out whether or not a type has a move constructor?

Assuming that the base class comes from the upstream, and the derived class is part of your application, there is no further decision you can make, once you decided to derive 'your' Bar from 'their' Foo.

It is the responsibility of the base class Foo to define its own constructors. That is an implementation detail of the base class. The same is true for the derived class. Constructors are not inherited. Trivially, both classes have full control over their own implementation.

So, if you want to have a move constructor in the derived class, just add one:

struct Bar : Foo {
   Bar(Bar&&) noexcept {
      std::cout << "move!" << std::endl;
   };
};

If you don't want any, delete it:

struct Bar : Foo {
   Bar(Bar&&) = delete;
};

If you do the latter, you can also uncomment the second static_assert without getting an error.