【问题标题】:Is this swap implementation without destruction valid according to the standard?根据标准,这种没有破坏的交换实现是否有效?
【发布时间】:2018-11-05 21:20:14
【问题描述】:

我建议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) 中使用移动分配,因为这意味着如果资源被持有,则执行代码以释放t1 中持有的资源,即使已知t1 中的资源已经释放。我有一个实际案例,其中通过虚拟方法调用释放资源,因此编译器无法消除不必要的工作,因为它不知道虚拟覆盖代码,无论它是什么,都不会做任何事情,因为没有在t1 中发布的资源。

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

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

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

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

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

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

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

【问题讨论】:

  • 移动分配比移动构造更快。而且我很确定如果您将 new 放入有效对象的内存中,那么该有效对象永远不会被破坏。
  • 根据 Rakete 下面提到的标准部分,在依赖于其析构函数的任何类型上使用此表单是 UB。
  • std::swap 也不会让用户认为构造函数会被调用,所以这种行为很容易引起误解。假设您的类型中有一个对象实例计数器..每次调用 swap 它都会增加 3?这对“交换”没有任何意义
  • @xaxxon 这是 libstdc++ 如何使用移动构造函数实现交换的链接:github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/…
  • auto lt1 = std::launder(&amp;t1);使得lt1可以访问新对象,但是对t1没有影响,所以还是UB使用t1访问新对象。

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


【解决方案1】:

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

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

但您还需要将std::launder 用于t1t2!这里有一个问题,因为没有std::laundert1t2 指的是旧的(已经被破坏的)值,而不是指新构造的对象。任何对t1t2 的访问都是UB。

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

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

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

【讨论】:

  • 他们还需要在调用new(&amp;t1) T{std::move(t2)}; 之前调用t1 的析构函数,不是吗? (同样的 Q 也适用于 t2)
  • @NathanOliver 他们实际上没有,但你是对的,如果你不这样做是个坏主意;)
  • 好的......所以重要的部分是“......任何依赖于析构函数产生的副作用的程序都有未定义的行为。”看来您应该在回答中明确指出这一点。
  • @xaxxon 我的理解是那部分没用。你不能依赖标准没有说明的东西。这就像有一句话说“任何依赖“1 + 1”调用函数 f() 的程序都有 UB。”
  • @Rakete1111 在定义良好的程序中,对象的析构函数的副作用必须在对象的生命周期结束时发生,对吧?因此,如果您要跳过调用析构函数,您必须先验地知道没有副作用 - 这在 std::swap 等通用代码中是一个糟糕的选择
【解决方案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)};

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

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

【讨论】:

  • "移动构造函数和赋值运算符必须保持其参数处于有效状态。" “有效”是什么意思?它们肯定是可破坏的,但实际上还有其他要求吗?
  • @moonshadow 如您所见,提议的swap 中的所有操作都使对象处于有效状态(仅发生有效对象的移动构造)。如果出现以下情况,则不可能发生资源泄漏:1. 移动构造函数不泄漏 2. 在移动构造中获取的资源没有作为移动构造的源传输(这将需要在析构函数处释放) ),我认为该场景(在构建时获得但未发布为移动构建源的“神奇资源”)不会过多地限制我的代码的用户
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-04
  • 1970-01-01
  • 1970-01-01
  • 2012-10-04
  • 2013-07-12
  • 2021-10-04
相关资源
最近更新 更多