【问题标题】:Force a compile time error if std::move will result in an unintended copy?如果 std::move 会导致意外复制,则强制编译时错误?
【发布时间】:2013-09-09 21:51:05
【问题描述】:

在他的 GoingNative 2013 演讲中,Scott Meyers 指出 std::move 并不能保证生成的代码会实际执行移动。

例子:

void foo(std::string x, const std::string y) {
  std::string x2 = std::move(x); // OK, will be moved
  std::string y2 = std::move(y); // compiles, but will be copied
}

在这里,不能应用移动构造函数,但由于重载解析,将使用普通的复制构造函数。这个回退选项对于向后兼容 C++98 代码可能至关重要,但在上面的示例中,它很可能不是程序员的意图。

有没有办法强制调用移动构造函数?

例如,假设您要移动一个巨大的矩阵。如果您的应用程序确实依赖于要移动的矩阵,那么在无法移动的情况下立即获得编译错误会很棒。 (否则,性能问题可能很容易在单元测试中溜走,只有经过一些 profiling 才能发现。)

让我们将这个有保证的移动称为strict_move。我希望能够编写这样的代码:

void bar(Matrix x, const Matrix y) {
  Matrix x2 = strict_move(x); // OK
  Matrix y2 = strict_move(y); // compile error
}

有可能吗?

编辑:

感谢您的精彩回答!有一些合法的要求可以澄清我的问题:

  • 如果输入为 const,strict_move 是否应该失败?
  • 如果结果不会导致实际的移动操作,strict_move 是否应该失败(即使复制可能与移动一样快,例如,const complex<double>)?
  • 两者都有?

我最初的想法非常模糊:我认为 Scott Meyers 的示例非常令人担忧,所以我想知道是否可以让编译器阻止此类意外复制。

Scott Meyers 在他的演讲中提到,一般编译器警告不是一种选择,因为它会导致大量误报。相反,我想与编译器进行交流,例如“我 100% 确定这必须始终导致移动操作,并且对于这种特定类型而言,副本过于昂贵”。

因此,我会随便说strict_move 在这两种情况下都应该失败。同时我不确定什么是最好的。另一个我没有考虑的方面是noexcept

在我看来,strict_move 的确切语义是开放的。任何有助于在编译时防止一些愚蠢错误而又没有严重缺陷的东西都可以。

【问题讨论】:

  • 制作您自己的move 版本来检查返回类型?
  • 等等,您是要检查输入是否不是 const,还是要检查实际移动是否会发生。因为第二个是个坏主意,下面的答案忽略了这种解释。
  • @MooingDuck 有趣,我没有详细考虑过这种差异,我只是想有一个防止无意行为的安全网。我同意阻止移动 const 对象更安全,但是反对另外检查移动构造函数是否可用并且会被调用的论点是什么?我可以理解,在库代码中使用这样的构造可能会使代码不灵活,因为它排除了所有没有移动构造函数的类,并且可能会导致转换问题。但是,仅在您自己的代码库的性能关键部分使用它有什么缺点?
  • @PhilippClaßen:因为它会导致代码不灵活,因为它会排除所有没有移动构造函数的类,并且可能会导致转换问题。
  • 如果你关心的主要是复制一个巨大的矩阵(你的类矩阵持有)的问题,那么使用 strict_move 并不能完全解决你的问题。 (如果您尝试移动 std::vector 并且您的矩阵移动构造函数可能会引发异常,那么编译器实际上会复制向量元素)。更多详情见我的回答

标签: c++ c++11 move-semantics


【解决方案1】:

我建议不要编写检测const 的通用strict_move。我认为这不是你真正想要的。你想让它标记一个const complex<double>,还是一个常量pair<int, int>?这些类型将尽可能快地复制它们。标记它们只会很烦人。

如果你想这样做,我建议改为检查类型是否为noexcept MoveConstructible。这对于std::string 非常有效。如果string的复制构造函数被意外调用,它不是noexcept,因此会被标记。但是如果pair<int, int>的拷贝构造函数被意外调用了,你真的在​​乎吗?

这是它的外观草图:

#include <utility>
#include <type_traits>

template <class T>
typename std::remove_reference<T>::type&&
noexcept_move(T&& t)
{
    typedef typename std::remove_reference<T>::type Tr;
    static_assert(std::is_nothrow_move_constructible<Tr>::value,
                  "noexcept_move requires T to be noexcept move constructible");
    static_assert(std::is_nothrow_move_assignable<Tr>::value,
                  "noexcept_move requires T to be noexcept move assignable");
    return std::move(t);
}

我决定也检查一下is_nothrow_move_assignable,因为您不知道客户端是在构造还是分配 lhs。

我选择了内部static_assert 而不是外部enable_if,因为我不希望noexcept_move 过载,并且static_assert 在触发时会产生更清晰的错误消息。

【讨论】:

  • OP 似乎并不太关心强制移动到 happen (无论如何这将是一个骗局),就像以某种方式意外使用 move这根本没有意义。
  • 也许我误解了这个问题。但是我的noexcept_move 的行为与我相信 OP 对 OP 的 foo 的期望相同。
  • 我不确定 OP 的实际意图。反正我投了赞成票。
【解决方案2】:

首先我想说的是,您之前的回答并不能完全解决您的问题。

以前的解决方案(@Kerrerk 和@0x499602D2)失败的反例:假设您使用引发异常的移动构造函数编写矩阵类。现在假设您要移动std::vector&lt;matrix&gt;This article 表明,如果 std::vector&lt;matrix&gt; 持有的矩阵类元素实际被移动(如果第 j 个元素移动构造函数抛出异常会发生什么?你会丢失数据,因为有无法恢复您已经移动的元素!)。

这就是为什么 stl 容器使用 std::move_if_noexcept 实现 .push_back().reserve() 和它们的移动构造函数来移动它们持有的元素。下面是从open-std 获取的reserve() 的示例实现:

void reserve(size_type n)
{
    if (n > this->capacity())
    {
        pointer new_begin = this->allocate( n );
        size_type s = this->size(), i = 0;
        try
        {
            for (;i < s; ++i)
                 new ((void*)(new_begin + i)) value_type( std::move_if_noexcept( (*this)[i]) ) );
        }
        catch(...)
        {
            while (i > 0)                 // clean up new elements
               (new_begin + --i)->~value_type();

            this->deallocate( new_begin );    // release storage
            throw;
        }
        // -------- irreversible mutation starts here -----------
        this->deallocate( this->begin_ );
        this->begin_ = new_begin;
        this->end_ = new_begin + s;
        this->cap_ = new_begin + n;
    }
}

所以,如果你不强制你的移动和默认构造函数是 noexcept,你将不能保证像 std::vector.resize() 或 std::move(对于 stl 容器)这样的函数永远不会复制矩阵类,这是正确的行为(否则您可能会丢失数据)

【讨论】:

  • 声明:在我编写/审查此解决方案时:@HowardHinnant 发布了一个类似的答案
  • +1 参考委员会文件。这就是寻找理由的地方。
【解决方案3】:

您可以制作自己的move 版本,它不允许使用常量返回类型。例如:

#include <utility>
#include <string>
#include <type_traits>


template <typename T>
struct enforce_nonconst
{
    static_assert(!std::is_const<T>::value, "Trying an impossible move");
    typedef typename std::enable_if<!std::is_const<T>::value, T>::type type;
};

template <typename T>
constexpr typename enforce_nonconst<typename std::remove_reference<T>::type>::type &&
mymove(T && t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type &&>(t);
}

void foo(std::string a, std::string const b)
{
    std::string x = std::move(a);
    std::string y = std::move(b);
}

void bar(std::string a, std::string const b)
{
    std::string x = mymove(a);
    // std::string y = mymove(b);  // Error
}

int main() { }

【讨论】:

    猜你喜欢
    • 2017-08-05
    • 2019-04-04
    • 1970-01-01
    • 1970-01-01
    • 2013-02-01
    • 1970-01-01
    • 2016-04-30
    • 2015-09-12
    • 1970-01-01
    相关资源
    最近更新 更多