c++ - 根据标准,这种没有破坏的交换实现是否有效?

标签 c++ language-lawyer standards swap c++-standard-library

我建议 swap 的这种实现(如果有效的话)优于 std::swap 的当前实现:

#include <new>
#include <type_traits>

template<typename T>
auto swap(T &t1, T &t2) ->
    typename std::enable_if<
        std::is_nothrow_move_constructible<T>::value
    >::type
{
    alignas(T) char space[sizeof(T)];
    auto asT = new(space) T{std::move(t1)};
        // a bunch of chars are allowed to alias T
    new(&t1) T{std::move(t2)};
    new(&t2) T{std::move(*asT)};
}

page for std::swap at cppreference ,意味着它使用移动赋值,因为 noexcept 规范取决于移动赋值是否为 no-throw。此外,这里已经有人问过how is swap implemented这就是我在 libstdc++ 的实现中看到的和 libc++

template<typename T>
void typicalImplementation(T &t1, T &t2)
    noexcept(
        std::is_nothrow_move_constructible<T>::value &&
        std::is_nothrow_move_assignable<T>::value
    )
{
    T tmp{std::move(t1)};
    t1 = std::move(t2);
        // this move-assignment may do work on t2 which is
        // unnecessary since t2 will be reused immediately
    t2 = std::move(tmp);
        // this move-assignment may do work on tmp which is
        // unnecessary since tmp will be immediately destroyed
    // implicitly tmp is destroyed
}

我不喜欢使用 t1 = std::move(t2) 中的移动分配,因为这意味着如果资源是,则执行代码以释放 t1 中保存的资源持有,尽管已知 t1 中的资源已被释放。我有一个实际情况,其中释放资源是通过虚拟方法调用发生的,因此编译器无法消除不必要的工作,因为它无法知道虚拟覆盖代码,无论它是什么,都不会执行任何操作,因为没有在 t1 中释放的资源。

如果这在实践中是非法的,您能否指出它违反了标准的哪些内容?

到目前为止,我在答案和评论中看到了两种可能使这种行为非法的反对意见:

  1. tmp 中创建的临时对象不会被破坏,但用户代码中可能存在一些假设,即如果构造了 T,它将被破坏
  2. T 可能是具有无法更改的常量或引用的类型,移动分配可以通过交换资源来实现,而无需触及这些常量或重新绑定(bind)引用。

因此,似乎此构造对于除上述情况 1 或 2 之外的任何类型都是合法的。

为了便于说明,我放置了一个指向 compiler explorer 的链接。页面显示了交换整数 vector 情况的所有三种实现,即 std::swap 的典型默认实现、专门用于 vector 的实现以及一个我提议。您可能会发现建议的实现比典型的实现执行的工作更少,与标准中的专门实现完全相同。

只有用户可以决定交换“全移动构建”与“一步构建,两次移动分配”,您的答案会告知用户“全移动构建”无效。

在与同事进行更多边带对话之后,我所要求的归结为这适用于移动被视为破坏性的类型,因此无需平衡构造与破坏。

最佳答案

如果 T 具有 ref 或 const 成员,则这是非法的。使用 std::launder 或存储新的指针(请参阅 [basic.life]p8 )

auto asT = new(space) T{std::move(t1)};
// or use std::launder

但是您还需要对 t1t2 使用 std::launder!这里有一个问题,因为如果没有 std::laundert1t2 会引用旧的(已经被破坏的)值并且不会' t 指的是新构造的对象。对 t1t2 的任何访问都是 UB。

I dislike using move-assignment in swap because it implies the destruction of already-moved objects, which seems unnecessary work, hence this implementation.

过早优化?现在您需要调用两个析构函数(t1t2)!而且,析构函数调用实际上并不昂贵。

现在,正如 Nathan Oliver 所说,析构函数不会被调用(这不是 UB),但你确实不应该这样做,因为析构函数可能会做重要的事情。

关于c++ - 根据标准,这种没有破坏的交换实现是否有效?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53162396/

相关文章:

java - 取消引用未使用的对象是否会提高冗长方法的性能?

c++ - 右值作为构造对象的初始化器

c++ - 任务内的 OpenMP 任务循环

c++ - 如何在给定范围内找到 std::set< pair<int, int>> 中的最大值?

c++ - 当编译时已知引用占用非聚合结构中的空间时,是否错过了优化?

c++ - 如果 Derived 不向 Base 添加新成员(并且是 POD),那么可以安全地完成什么样的指针转换和取消引用?

c++ - 如何使用 G++ 抑制纯虚拟类的 C++ vtable 生成?

c++ - Visual Studio 中缺少析构函数?

user-interface - 如何制定和应用 UI 开发标准?

css - 哪个浏览器返回 SVG 元素的 getBoundingClientRect 的正确结果?