【问题标题】:How to get if a type is truly move constructible如何获得类型是否真正可移动构造
【发布时间】:2019-01-24 21:02:57
【问题描述】:

以这段代码为例:

#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&amp;&amp;,与S&amp; 类型的引用兼容。因此S x(std::move(y)) 完全有效并调用复制构造函数S::S(const S&amp;)

——莱恩,Understanding std::is_move_constructible

所以我明白为什么右值从移动到左值复制“降级”,因此为什么std::is_move_constructible 返回真。但是,有没有办法检测一个类型是否真正可移动构造,不包括复制构造函数?

【问题讨论】:

  • 我建议寻找一种方法来检查给定类型是否具有具有特定签名的成员函数——很确定有一种方法可以做到这一点(需要一些元编程魔法)。喜欢this。您也许可以将其用于构造函数...
  • 这是一个 XY 问题。为什么需要知道是否有定义的移动构造函数?您打算如何使用这些信息?
  • 您可以通过简单地不编写可复制但不可移动的类来轻松避免此问题。
  • @PasserBy 我在这里说得更笼统。我不知道OP。但一般来说,移动对象可能“快得可以接受”,而复制则不然。 (即,如果对象拥有千兆字节的数据)在这种情况下,当它无法移动而不是无意允许大副本时,编译时错误将很有用。
  • 即使有移动构造函数也不意味着它不复制!也就是说,编译器肯定会相信这个类是“真正可移动构造的:struct baz: foo { baz(baz const&amp;) = default; baz(&amp;&amp; other): baz(other) {} }; 顺便说一句,这种行为与您的 bar 相同,但您似乎希望对这个问题有不同的答案。

标签: c++ stl move-semantics typetraits


【解决方案1】:

有人声称presence of move constructor can't be detected 和表面上它们似乎是正确的——&amp;&amp; 绑定到const&amp; 的方式使得无法判断哪些构造函数存在于类的接口中。

然后我想到了——C++ 中的移动语义不是单独的语义......它是复制语义的“别名”,是类实现者可以“拦截”并提供替代实现的另一个“接口”。所以问题是“我们可以检测到移动 ctor 的存在吗?”可以重新表述为“我们可以检测到两个复制接口的存在吗?”。事实证明,我们可以通过(ab)使用重载来实现这一点——当有两种同样可行的方法来构造一个对象并且可以使用 SFINAE 检测到这一事实时,它无法编译。

30 lines of code 值一千字:

#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 是假的:/)
  • @JonathanGawrych 我检查了 cctor/mctor/dtor 的每个排列,它似乎产生了正确的结果。答案更新为稍微更精致的版本。不知道 MSVC... 它总是有点迟钝。
【解决方案2】:

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

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

基类 Foo 负责定义自己的构造函数。这是基类的实现细节。派生类也是如此。构造函数不被继承。很简单,两个类都可以完全控制自己的实现。

所以,如果你想在派生类中有一个移动构造函数,只需添加一个:

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

如果你不想要,删除它:

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

如果您执行后者,您还可以取消注释第二个 static_assert 而不会出错。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 2017-11-24
    • 1970-01-01
    • 2017-03-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多