How to get if a type is truly move constructible
Asked Answered
J

2

8

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?

Jolly answered 17/8, 2018 at 19:33 Comment(10)
I suggest looking for a way to check if given type has member function with specific signature -- pretty sure there is a way to do this (some metaprogramming magic is required). Smth like this. You might be able to adapt it for constructors...Vladimir
This is an XY problem. Why do you need to know whether there is a defined move constructor? How do you plan to use this information?Preparative
You could easily avoid this problem by simply not writing classes that are copyable-but-not-movable.Fustigate
@PasserBy I can see this being useful for avoiding unintended and unnecessary copies from a performance standpiont.Polonium
@Brian What if the type trait is to make sure Bar suddently doesnt become copyable? (In case someone else changes Foo?) "Just dont write bugs" seems a bit dismissive to me...Zelikow
@Zelikow then you can static_assert that it's not copy-constructible. What's the use case for the code shown in the question as it currently reads?Fustigate
@Polonium The issue is, there is no way to know whether a move is acceptably fast while a copy isn't by this test. You can't verify this is what you want from a performance standpoint.Preparative
@PasserBy I'm speaking more generally here. I don't know about the OP. But in general, it is possible that moving an object is "acceptably fast" while copying it isn't. (i.e. if the object holds gigabytes of data) In which case, it would useful to compile-time error when it can't be moved instead of unintentionally allowing a large copy.Polonium
Even if there is a move constructor it doesn’t mean it doesn’t copy! That is, the compiler will certainly believe this class is “truly move constructible: struct baz: foo { baz(baz const&) = default; baz(&& other): baz(other) {} }; Incidently, this behavior is identical to your bar but it seems you expect somehow a different answer to the question.Herculie
As for my use case, I want to write a wrapper that can take a template type then noisily print which of the default methods (copy/move construct/assign etc) are being used. However it'd be "lying" saying "move" when the wrapped class doesn't have a move and was actually copied. Also just general interest in if this is a possibility, or if it's impossible to detect.Jolly
V
10

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)

Vladimir answered 18/8, 2018 at 21:57 Comment(4)
Very interesting! This is quite a cool solution. I've tested it on gcc and clang, which both work; however, msvc doesn't (it always thinks has_mctor is false :/)Jolly
@JonathanGawrych I checked every permutation of cctor/mctor/dtor and it seem to be producing correct results. Answer is updated with slightly more polished version. No idea about MSVC... It was always slightly retarded.Vladimir
this is clever. thank you so much for this!Twannatwattle
Works nicely. I'm using it in an abi checking hack. Also generalizes to assignment as: constexpr bool has_move_assign = std::is_move_assignable_v<T> && !std::is_assignable_v<T, M<T>>;Nonsectarian
A
0

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.

Agathaagathe answered 17/8, 2018 at 21:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.