为什么 std::swap 不使用 swap 习语?

Why is std::swap not using swap idiom?

本文关键字:swap 习语 std 为什么      更新时间:2023-10-16

因为正确使用std::swap是:

using std::swap;
swap(a,b);

这有点冗长,但它确保如果 a,b 定义了更好的交换,它就会被选中。

所以现在我的问题是为什么不使用这种技术实现std::swap所以用户代码只需要调用std::swap

所以像这样的事情(为了简洁起见,忽略noexcept和约束):

namespace std {
namespace internal {
template <class T>      // normal swap implementation
void swap(T& a, T& b) { // not intended to be called directly
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
}
template <class T>
void swap(T& a, T& b) {
using internal::swap;
swap(a,b);
}
}

这是进入重言式领域,但它不是那样做的,因为这不是它的目的。

std::swap的目的是成为最后手段的交换功能。它不应该是你直接调用的东西,除非你真正想要的是使用最后手段的交换。

现在,您可以争辩说,您建议的是自定义点的更好范例。事后诸葛亮总是 20/20;并非STL所做的一切都是正确的想法(见vector<bool>)。

至于为什么我们现在不能改变这一点,那是另一回事了。std::swap是最后手段的交换功能。因此,从理论上讲,人们可能会调用它并期望它绕过任何用户定义的交换代码。因此,以这种方式更改它会破坏他们的代码。

这是这种方法的一个问题。ADL"两步"依赖于 ADL 发现比正常匹配更好的函数(否则,会出现过载解析失败)。这在大多数情况下都很好,因为当您在用户定义的命名空间中为用户定义类型编写swap()时,您正在编写特定于这些类型的函数。

但是对于标准库类型,可能没有比简单算法更有效swap(),这被分解了。例:

namespace N {
namespace internal {
template <typename T>
void swap(T&, T&); // normal swap impl
}
template <typename T>
void swap(T& a, T& b) {
using internal::swap;
swap(a, b); // (*)
}
struct C { };
}
N::C c;
swap(c, c);    // error
N::swap(c, c); // error

出于同样的原因,这两个调用都失败了。对标记为(*)swap()的非限定调用将通过正常的非限定查找查找N::internal::swap(),然后通过 ADL 查找N::swap()。没有办法区分这些调用,因为你非常需要这两个调用都适用于满足swap约束的所有类型。生成的调用不明确。

所以这样的设计需要为namespace std中的每个类型添加一个新的swap()函数,即使它只是转发到std::internal::swap()

从历史上看,似乎对名称解析没有太多考虑。std::swap被设计为一个自定义点,但也最终被设计为在通用代码中调用以交换事物的函数。因此,如果std::swap不起作用,或者太慢,那么即使 ADL 已经有一个非常好的swap,也可能不得不重载它。如果不破坏或更改现有代码的含义,则无法更改此设计。现在有些情况下,为了性能起见,委员会很高兴地决定改变现有代码的含义,例如隐式移动语义(例如,按值传递临时代码时,或未实现省略的 RVO)。所以这并不完全一致。尽管如此,将std::swap从自定义点更改为具有所有现有std::swap重载的名称解析包装器是可疑的。这绝对可能导致在编写不佳的遗留代码中触发灾难性错误。

另一个重要原因是,IMO,你不能在保持其通用性的同时将此习惯用法移动到std命名空间。 例如:

namespace A { struct X{}; }
namespace B {
using std::swap;
void swap(A::X&, A::X&);
template<typename T>
void reverse(T (&ts)[4])
{
swap(ts[0], ts[3]);
swap(ts[1], ts[2]);
}
void silly(A::X (&xs)[4])
{
reverse(xs);
}
}

在这里,silly最终使用B::swap.这样做的原因是std::swap(通过using)和B::swap都以相同的优先级可见,但后者是更好的匹配。现在,你可能会争辩说这是一个愚蠢的例子,所以这里有另一个不那么做作的例子:

namespace types { /*...*/ }
namespace algorithms { /*including some swap implementations for types from above...*/ }
template<typename T>
void reverse(T (&ts)[4])
{
using std::swap;
using algorithms::swap;
swap(ts[0], ts[3]);
swap(ts[1], ts[2]);
}

这将使用algorithms的交换函数,如果它比任何std::swap重载都匹配得更好,但 ADL 不会找到algorithms::swap,因此查找通常不能在std::swap内部进行。此处可能涉及任意数量的命名空间。