根据标准,这种无破坏的交换实现是否有效?

时间:2018-11-05 21:20:15

标签: 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的规范取决于移动分配是否为无掷。此外,在这里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)中那样使用移动分配,因为这意味着即使持有{{1}中的资源,也要执行代码以释放持有t1中的资源。 }已发布。我有一个实际的案例,其中在虚拟方法调用之间释放资源,因此编译器无法消除不必要的工作,因为它不知道虚拟重写代码,无论它是什么,都不会做任何事情,因为没有在t1中释放的资源。

如果在实践中这是非法的,您能指出它在标准中违反了什么吗?

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

  1. t1中创建的临时对象没有被破坏,但是在用户代码中可能存在一些假设,即如果构建了tmp,它将被破坏
  2. T可能是带有不能更改的常量或引用的类型,可以实现移动分配以交换资源而不接触那些常量或重新绑定引用。

因此,该构造似乎对任何类型都是合法的,除了那些符合上述情况1或2的情况。

为说明起见,我将链接指向compiler explorer页面,该页面显示了交换int向量的情况的所有三种实现,即T的典型默认实现,专门用于{ {1}}和我提议的那个。您可能会看到建议的实现所执行的工作比典型的要少,与标准中的完全相同。

只有用户可以决定互换执行“一动一建”与“一动一建,两步分配”,您的答案会通知用户“一动一建”无效。

在与同事进行更多的边带对话之后,我要问的是归结为这种方法对于那些可以被视为具有破坏性的移动类型,因此不需要在结构与破坏之间取得平衡。

2 个答案:

答案 0 :(得分:2)

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

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

但是您还需要对std::laundert1使用t2!这就是问题所在,因为如果没有std::laundert1t2,则引用旧的(已经破坏的)值,而不引用新构造的对象。这样,对t1t2的任何访问都将成为UB。

  

我不喜欢在swap中使用移动分配,因为这意味着破坏已经移动的对象,这似乎是不必要的工作,因此是该实现。

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

目前,正如Nathan Oliver所说的那样,没有调用析构函数(不是UB),但是您确实不应该这样做,因为析构函数可能会做重要的事情。

答案 1 :(得分:2)

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

例如考虑:

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)};

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

查看此问题的另一种方式是,要么销毁什么都不做,要么便宜/免费,所以不能证明神秘的肉类优化是合理的,或者销毁足够昂贵,可以解决,在这种情况下,它是做某事,因此在对象背后躲避做那件事在道德上是错误的,并会导致不良后果。修复对象实现。