当定义自定义点对象时,为什么删除函数是必要的?

16

自libstdc++的<concepts>头文件:

  namespace ranges
  {
    namespace __cust_swap
    {
      template<typename _Tp> void swap(_Tp&, _Tp&) = delete;

来自 MS-STL <concepts> 头文件:

namespace ranges {
    namespace _Swap {
        template <class _Ty>
        void swap(_Ty&, _Ty&) = delete;

我从未在禁止调用复制/移动赋值/构造函数的上下文之外遇到过= delete;

我很好奇这是否是必要的,所以我已经将库中的= delete;部分注释掉了:

// template<typename _Tp> void swap(_Tp&, _Tp&) = delete;

检查以下测试用例是否编译。

#include <concepts>
#include <iostream>

struct dummy {
    friend void swap(dummy& a, dummy& b) {
        std::cout << "ADL" << std::endl;
    }
};

int main()
{
    int a{};
    int b{};
    dummy c{};
    dummy d{};
    std::ranges::swap(a, b);
    std::ranges::swap(c, d); // Ok. Prints "ADL" on console.
}

不仅编译通过,而且似乎通过调用用户定义的swap函数来很好地表现出了对struct dummy的操作。因此我在想:

  1. 在这个上下文中,template<typename _Tp> void swap(_Tp&, _Tp&) = delete;到底是做什么的?
  2. 在什么情况下会没有 template<typename _Tp> void swap(_Tp&, _Tp&) = delete;而导致错误?

5
在这个上下文中,"template<typename _Tp> void swap(_Tp&, _Tp&) = delete;"的作用是禁止编译器使用默认的swap实现。 - Language Lawyer
1
请注意,=delete 语法也可用于隐藏非虚继承函数。(这不是本例中发生的情况,但它是除删除任何隐式定义的构造函数或赋值运算符之外的另一种用途。) - cdhowie
2个回答

12
TL;DR:它存在是为了避免调用std::swap
这实际上是ranges::swap自定义点的一个明确要求:

S is (void)swap(E1, E2) if E1 or E2 has class or enumeration type ([basic.compound]) and that expression is valid, with overload resolution performed in a context that includes this definition:

 template<class T>
  void swap(T&, T&) = delete;
所以这个做什么?为了理解这一点,我们必须记住ranges命名空间实际上是std::ranges命名空间。这很重要,因为很多东西都存在于std命名空间中,包括这个,在<utility>中声明:
template< class T >
void swap( T& a, T& b );

可能会有constexprnoexcept,但这对我们的需求不相关。作为自定义点,std::ranges::swap有一种特定的方式来进行自定义。它希望您提供一个swap函数,该函数可以通过参数依赖查找找到。这意味着ranges::swap将通过执行swap(E1, E2)来查找您的交换函数。这很好,除了一个问题: std::swap存在。在C++20之前,使类型可交换的一种有效方法是为std::swap模板提供专门化。因此,如果您直接调用std::swap来交换某些内容,则会选择并使用您的专门化。但是,ranges::swap不想使用它们。它有一种自定义机制,并且希望您非常明确地使用该机制,而不是std::swap的模板专门化。但是,由于std::ranges::swap位于std命名空间中,未经限定的对swap(E1, E2)的调用可能会找到std::swap。为了避免找到和使用此重载,它通过使一个版本成为= delete来使该重载受到限制。因此,如果您未为类型提供ADL可见的swap,则会出现硬错误。适当的定制还需要比std::swap版本更加专业化(或更受限制),以便可以将其视为更好的重载匹配项。请注意,ranges::begin/end和类似函数具有类似的措辞,以关闭类似命名的std::函数的类似问题。

2
在使用声明的使用点不会影响 CPO 内部的查找。 - T.C.
@T.C.:我写了一个测试程序来调查这个问题,但实际上我没有看到如果我去掉using声明会发生什么。所以你是对的;它存在的原因仍然类似于我说的,但不同之处在于它可以避免直接调用std::swap,因为std::swapstd::ranges::swap在同一命名空间中,因此对其可见。我已经编辑答案进行更正。 - Nicol Bolas
实现可以从std之外的某个地方(丑陋)执行查找;更多地避免了::swap - Davis Herring

9

毒丸过载有两个动机,其中大多数实际上已经不存在了,但我们仍然保留它们。

swap/iter_swap

P0370所述:

The Ranges TS has another customization point problem that N4381 does not cover: an implementation of the Ranges TS needs to co-exist alongside an implementation of the standard library. There’s little benefit to providing customization points with strong semantic constraints if ADL can result in calls to the customization points of the same name in namespace std. For example, consider the definition of the single-type Swappable concept:

namespace std { namespace experimental { namespace ranges { inline namespace v1 {
  template <class T>
  concept bool Swappable() {
    return requires(T&& t, T&& u) {
      (void)swap(std::forward<T>(t), std::forward<T>(u));
    };
  }
}}}}

unqualified name lookup for the name swap could find the unconstrained swap in namespace std either directly - it’s only a couple of hops up the namespace hierarchy - or via ADL if std is an associated namespace of T or U. If std::swap is unconstrained, the concept is “satisfied” for all types, and effectively useless. The Ranges TS deals with this problem by requiring changes to std::swap, a practice which has historically been forbidden for TSs. Applying similar constraints to all of the customization points defined in the TS by modifying the definitions in namespace std is an unsatisfactory solution, if not an altogether untenable.

Range TS是基于C++14构建的,其中std::swap没有限制(std::swap直到C++17采用P0185才变得有限制),因此很重要确保Swappable不仅仅对于任何具有std作为关联命名空间的类型都可以简单地解决为true。
但现在std::swap已经受到限制,因此没有必要使用swap毒丸。
然而,std::iter_swap仍然没有限制,因此仍然需要使用该毒丸。但是,那个也可以轻松地变得有限制,然后我们再次不需要毒丸。
开始/结束
P0970所述:
为了与std :: begin兼容并方便迁移,std :: experimental :: ranges :: begin接受rvalue并将其视为const lvalue。这种行为已被弃用,因为它基本上是不合理的:由这样的重载返回的任何迭代器都很可能在包含调用begin的完整表达式之后悬挂。
另一个问题,直到最近似乎与begin的设计无关,即返回迭代器的算法将在传递给它们的范围是rvalue时将这些迭代器包装在std :: experimental :: ranges :: dangling <>中。这忽略了某些范围类型(特别是P0789的subrange <>>)的事实:迭代器的有效性根本不取决于范围的生命周期。在传递prvalue subrange <>到算法的情况下,返回包装的迭代器是完全不必要的。
[...]
我们认识到,通过从范围访问定制点中删除对rvalues的弃用默认支持,我们为范围作者提供了设计空间,以选择此行为适用于其范围类型,从而向算法传达迭代器可以安全地超出其范围类型。这消除了在传递rvalue subrange时需要dangling的情况,这是一个重要的使用场景。
这篇论文提出了一种安全调用 rvalue 的 begin 的设计,它是一个非成员函数,专门接受一个 rvalue。存在的意义在于:
template <class T>
void begin(T&&) = delete;

过载:

赋予std2::begin这样的属性,对于某些类型为T的右值表达式E,表达式std2::begin(E)只有在通过ADL找到特别接受类型为T的右值的自由函数begin并且该重载优先于一般的void begin(T&&)“毒药”重载时才会编译。

例如,这将允许我们正确拒绝在类型为std::vector<int>的右值上调用ranges::begin,即使非成员std::begin(const C&)可以被ADL找到。

但是这篇文章也说:

作者认为解决“子范围”和“悬挂指针”的问题需要增加一个新的特性,以使范围类型的作者有一种方法来表明其迭代器是否可以安全地超出范围。这感觉像是一个hack,而且作者无法选定这样一个特性的名称,这个名字既简洁又清晰。随后,这个功能已经通过一个特性进行检查 - 最初称为“enable_safe_range”(P1858),现在称为“enable_borrowed_range”(LWG3379)。因此,这里不再需要毒药丸。

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