首页 > 解决方案 > 根据标准,这种没有破坏的交换实现是否有效?

问题描述

如果有效,我建议此实现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)};
}

at cppreference页面std::swap暗示它使用移动分配,因为noexcept规范取决于移动分配是否为无抛出。此外,这里有人问过它是如何swap实现的,这就是我在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不会被破坏,但用户代码中可能存在一些假设,即如果 aT被构造,它将被破坏
  2. T可能是具有无法更改的常量或引用的类型,移动分配可以通过交换资源而不触及这些常量或重新绑定引用来实现。

因此,似乎这个构造对于任何类型都是合法的,除了上面的情况 1 或 2。

为了说明,我放置了一个指向编译器资源管理器页面的链接,该页面显示了交换整数向量的所有三种实现,即 的典型默认实现std::swap、专用的 forvector和我提议的那个。您可能会看到建议的实现执行的工作比典型的要少,与标准中的专用实现完全相同。

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

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

标签: c++language-lawyerstandardsswapc++-standard-library

解决方案


请注意,移动构造函数和赋值运算符需要将其参数保持在有效状态。通常,实现将默认构造状态,然后与参数的状态交换以窃取其资源。根据对象想要维护的不变量,这可能仍然让参数拥有它依赖析构函数回收的资源。如果忽略破坏,这些将泄漏。

例如考虑:

class X
{
public:
    X(): resource_( std::move( allocate_resource() ) )

    X( X&& other ): X()
    {
        std::swap( resource_, other.resource_ );
    }

private:
    std::shared_ptr<Y> resource_;
};

然后,

X a;
X b;
swap( a, b );

现在,如果按照您的建议实施交换,那么在您做的时候

new(&t2) T{std::move(*asT)};

我们泄漏了资源的一个实例,因为移动构造函数在 *asT 分配了一个来替换它窃取的那个,并且它永远不会被破坏。

另一种看待这个问题的方式是,要么销毁什么都不做,因此便宜/免费,因此不能证明神秘肉优化的合理性,或者销毁的成本足以关心,在这种情况下,它正在做某事,所以落后了对象的背部避免做那件事在道德上是错误的,并且会导致坏事;而是修复对象实现。


推荐阅读