如何判断一个类型是否真正支持移动构造

8
例如,考虑以下代码:

#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!"
}

因为 Bar 类是从 Foo 类派生而来,所以它没有移动构造函数。但是可以使用复制构造函数来构造它。我从另一个答案中了解到了选择复制构造函数的原因:
如果 y 是类型 S,则 std::move(y) 的类型为 S&&,与类型 S& 兼容。因此,S x(std::move(y)) 是完全有效的,并调用复制构造函数 S::S(const S&)。
因此,我理解了为什么右值“降级”为左值复制,因此为什么 std::is_move_constructible 返回 true。然而,有没有一种方法来检测一个类型是否真正可移动构造,而不包括复制构造函数?

我建议寻找一种方法来检查给定类型是否具有特定签名的成员函数--我相信有一种方法可以做到这一点(需要一些元编程魔法)。类似于这个。您可能能够将其适应于构造函数... - C.M.
这是一个 XY 问题。你为什么需要知道是否有定义的移动构造函数?你计划如何使用这些信息? - Passer By
1
@PasserBy,我这里说得更为一般化。我不知道原帖作者的情况。但通常情况下,移动一个对象可能是“可以接受的快速”,而复制则不然(即如果该对象保存着千兆字节的数据)。这种情况下,当无法移动时,编译时错误将非常有用,而不是无意中允许大量复制。 - Mysticial
1
即使有移动构造函数,也不意味着它不会复制!也就是说,编译器肯定会认为这个类是“真正的可移动构造的:struct baz: foo { baz(baz const&) = default; baz(&& other): baz(other) {} }; 顺便说一下,这种行为与您的 bar 相同,但似乎您希望以某种方式得到不同的答案。 - Dietmar Kühl
就我的使用情况而言,我想编写一个包装器,可以接受模板类型,然后嘈杂地打印正在使用哪些默认方法(复制/移动构造/分配等)。但是如果被包装的类没有移动操作并且实际上是复制的,那么说“移动”是“虚假”的。此外,我对是否可能检测到这一点感兴趣,或者它是否不可能实现。 - jgawrych
显示剩余5条评论
2个回答

9
有人声称移动构造函数的存在无法被检测到,表面上看起来他们是正确的——&&const&绑定的方式使得无法判断类接口中存在哪些构造函数。
然后我想到——在C++中,移动语义不是单独的语义...它是复制语义的“别名”,另一种类实现者可以“拦截”并提供替代实现的“接口”。因此,“我们能否检测到移动构造函数的存在?”这个问题可以重新表述为“我们能否检测到两个复制接口的存在?”结果我们可以通过(滥用)重载来实现这一点——当有两种同样可行的方法来构造一个对象时,编译会失败,而这个事实可以通过SFINAE来检测。 30行代码胜过千言万语:
#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);
}

注释:

  • 你可能需要使用额外的const/volatile重载来混淆这个逻辑,因此可能需要进行额外的工作。

  • 怀疑这个魔法在私有/受保护构造函数方面效果不佳——还需要进一步研究。

  • 似乎在MSVC上不起作用(一如既往)。


非常有趣!这是一个相当酷的解决方案。我已经在gcc和clang上测试过了,它们都可以工作;然而,msvc不行(它总是认为has_mctor是false :/)。 - jgawrych
1
@JonathanGawrych 我检查了每个 cctor/mctor/dtor 的排列组合,似乎它们都产生了正确的结果。答案已经更新并稍微进行了梳理。对于 MSVC 我不是很清楚……它总是有点迟钝。 - C.M.
这真是聪明。非常感谢你! - Kobi
运行良好。我正在使用它进行ABI检查的hack。 同时也可以推广到赋值操作: constexpr bool has_move_assign = std::is_move_assignable_v<T> && !std::is_assignable_v<T, M<T>>; - textshell

0

如何判断一个类型是否具有移动构造函数?

假设基类来自上游,派生类是您的应用程序的一部分,一旦您决定从“他们”的Foo派生“您”的Bar,就没有进一步的决策可以做。

定义自己的构造函数是基类Foo的责任。这是基类的实现细节。对于派生类也是如此。构造函数不会被继承。显然,两个类都完全控制自己的实现。

因此,如果您想在派生类中拥有移动构造函数,只需添加一个即可:

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

如果你不需要它,就删除它:

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

如果你选择后者,你也可以取消注释第二个 static_assert 而不会出现错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接