移动语义 == 自定义交换函数过时?

36

最近,许多 问题 涌现 关于如何提供自己的swap函数。在C++11中,std::swap将使用std::move和移动语义尽可能快地交换给定的值。当然,这仅在您提供了移动构造函数和移动赋值运算符(或使用按值传递的运算符)时才有效。

那么,考虑到这一点,是否真的有必要在C++11中编写自己的swap函数?我只能想到不可移动类型,但是再次说一遍,自定义的swap通常通过某种“指针交换”(即移动)来工作。也许有些参考变量?嗯...

5个回答

26
这是一个判断的问题。对于原型代码,我通常会让std::swap来处理,但是对于发布的代码则需要编写自定义的交换函数。我通常可以编写一个自定义的交换函数,其速度大约是1个移动构造+2个移动赋值+1个无资源销毁的2倍。不过,在出现性能问题之前,我们可能需要等待std::swap真正成为一个问题。
针对Alf P. Steinbach的更新: 20.2.2 [utility.swap] 指定std::swap(T&, T&)具有等效于noexcept的特点:
template <class T>
void
swap(T& a, T& b) noexcept
                 (
                    is_nothrow_move_constructible<T>::value &&
                    is_nothrow_move_assignable<T>::value
                 );

也就是说,如果对于 T 类型的移动操作是 noexcept 的话,那么对于 T 类型的 std::swap 也是 noexcept 的。

需要注意的是,该规范并不要求存在移动成员。它只要求从右值构造和赋值,并且如果它是 noexcept 的,则交换操作将是 noexcept 的。例如:

class A
{
public:
    A(const A&) noexcept;
    A& operator=(const A&) noexcept;
};

std::swap<A>是noexcept的,即使没有移动成员也是如此。


谢谢更新,霍华德。我猜A(const A&) noexcept;是一个打字错误,或者可能是SO吞噬了&符号?也就是说,你的意思是A(A&&) noexcept;(赋值运算符也一样)?干杯! - Cheers and hth. - Alf
3
@Alf:不是;他举了一个例子,即使没有实际的移动构造/赋值,std::swap 也会成为 noexcept(true) - Dennis Zickefoose
@Dennis:我认为我需要更多的解释,因为在我看来,那看起来像是一个普通(非移动)复制构造函数和一个普通的复制赋值运算符,而据我所知,这与is_nothrow_move_constructibleis_nothrow_move_assignable没有任何关系? - Cheers and hth. - Alf
2
@Alf:is_nothrow_move_constructible<T>is_nothrow_constructible<T,T&&>相同。传统的T(const T&)版本的复制构造函数可以很好地接受r-value引用,因此即使实际上没有移动,T也会报告为可移动构造,类似的映射从is_nothrow_move_assignable<T>is_nothrow_assignable<T, T&&>也是如此,因此相同的论点适用于那里。 - Dennis Zickefoose
@Dennis:感谢您向大家解释,您说得完全正确。我现在才回来看这个。 - Howard Hinnant
显示剩余2条评论

3
当然,你可以将swap实现为:
template <class T>
void swap(T& x, T& y)
{
  T temp = std::move(x);
  x = std::move(y);
  y = std::move(temp);
}

但是我们可能有自己的类,比如说 A,我们可以更快地进行交换。

void swap(A& x, A& y)
{
  using std::swap;
  swap(x.ptr, y.ptr);
}

这个方法不需要运行构造函数和析构函数,只需交换指针(可能是通过XCHG或类似的方式实现)。

当然,编译器可能会优化掉第一个例子中的构造函数/析构函数调用,但如果它们具有副作用(即对new/delete的调用),则可能无法聪明地将它们优化掉。


如果您的移动构造函数/赋值运算符具有未在自定义交换函数中反映的副作用,我怀疑您已经实现错误了。 - Dennis Zickefoose

1
可能有一些类型可以交换但不能移动。我不知道任何不可移动的类型,所以我没有任何例子。

1
std::array<int>是可复制的,但没有移动构造函数,如果这就是你的意思。 - Johan Lundberg
一个非可移动的东西:Win32 API 中的 CRITICAL_SECTION 看起来就像一个。它是一种不透明类型,必须被初始化,不能被复制,并且内部有一个指向自身的指针。要围绕它创建可移动的包装器,必须要有一个指针来管理它。 - Joker_vD

0
考虑以下类,它持有一个内存分配的资源(为简单起见,用一个整数表示):
class X {
    int* i_;
public:
    X(int i) : i_(new int(i)) { }
    X(X&& rhs) noexcept : i_(rhs.i_) { rhs.i_ = nullptr; }
 // X& operator=(X&& rhs) noexcept { delete i_; i_ = rhs.i_;
 //                                  rhs.i_ = nullptr; return *this; }
    X& operator=(X rhs) noexcept { swap(rhs); return *this; }
    ~X() { delete i_; }
    void swap(X& rhs) noexcept { std::swap(i_, rhs.i_); }
};

void swap(X& lhs, X& rhs) { lhs.swap(rhs); }

然后,std::swap 会导致删除 null 指针 3 次(包括移动赋值运算符和统一赋值运算符)。编译器可能无法优化这样的 delete,参见https://godbolt.org/g/E84ud4

自定义的 swap 不会调用任何 delete,因此可能更有效率。我猜这就是为什么 std::unique_ptr 提供自定义的 std::swap 特化的原因。

更新

似乎 Intel 和 Clang 编译器能够优化出空指针的删除,然而 GCC 不行。详情请参见Why GCC doesn't optimize out deletion of null pointers in C++?

更新

在 GCC 中,我们可以通过以下方式重写 X 来避免调用 delete 运算符:

 // X& operator=(X&& rhs) noexcept { if (i_) delete i_; i_ = rhs.i_;
 //                                  rhs.i_ = nullptr; return *this; }
    ~X() { if (i_) delete i_; }

0
按照惯例,自定义的"swap"函数应该提供无抛出异常的保证。至于"std::swap"函数,我对委员会在这方面的工作印象是纯粹政治,所以如果他们在某处将"duck"定义为"bug"或类似的政治词语操作,我也不会感到意外。因此,除非回答能够详细引用C++0x即将成为标准的内容,直到最小的细节(以确保没有"bug"),否则我不会依赖于任何回答中的信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接