为什么在定义自定义点对象时需要删除函数?

Lew*_*man 11 c++ stl language-lawyer c++20 customization-point

从 libstdc++<concepts>头文件:

  namespace ranges
  {
    namespace __cust_swap
    {
      template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
Run Code Online (Sandbox Code Playgroud)

从 MS-STL<concepts>标头:

namespace ranges {
    namespace _Swap {
        template <class _Ty>
        void swap(_Ty&, _Ty&) = delete;
Run Code Online (Sandbox Code Playgroud)

我从未遇到过= delete;要禁止调用复制/移动赋值/ctor 的上下文之外的情况。

我很好奇这是否有必要,所以我= delete;像这样注释了库中的部分:

// template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
Run Code Online (Sandbox Code Playgroud)

看看下面的测试用例是否编译。

#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.
}
Run Code Online (Sandbox Code Playgroud)

它不仅可以编译,而且通过调用用户定义swap的struct dummy. 所以我想知道,

  1. 是什么 template<typename _Tp> void swap(_Tp&, _Tp&) = delete;在这方面究竟做什么?
  2. 这在什么情况下没有中断 template<typename _Tp> void swap(_Tp&, _Tp&) = delete;?

Nic*_*las 8

TL;DR:这是为了避免调用std::swap.

这实际上是一个明确的要求的ranges::swap定制点:

S是(void)swap(E1, E2)ifE1或E2具有类或枚举类型 ([basic.compound]) 并且该表达式有效,并在包含此定义的上下文中执行重载解析:

 template<class T>
  void swap(T&, T&) = delete;
Run Code Online (Sandbox Code Playgroud)

那么这有什么作用呢?要理解这一点,我们必须记住ranges名称空间实际上是std::ranges名称空间。这很重要,因为很多东西都存在于std命名空间中。包括这个,声明于<utility>:

template< class T >
void swap( T& a, T& b );
Run Code Online (Sandbox Code Playgroud)

某处可能有一个constexprand noexcept,但这与我们的需求无关。

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. 为了避免发现和使用这个重载,它通过使= deleted的版本可见来毒化重载。因此,如果您没有swap为您的类型提供 ADL-visible ,则会出现严重错误。还需要适当的定制比std::swap版本更专业(或更受约束),以便可以将其视为更好的重载匹配。

请注意,ranges::begin/end和 相似的函数具有相似的措辞,以关闭具有相似名称的std::函数的相似问题。

  • CPO 内部的查找永远不会受到使用时的 using 声明的影响。 (2认同)

Bar*_*rry 7

毒丸超载有两个动机,其中大部分实际上不再存在,但无论如何我们仍然拥有它们。

交换 / iter_swap

如P0370 所述:

Ranges TS 有另一个 N4381 没有涵盖的定制点问题:Ranges TS 的实现需要与标准库的实现共存。如果 ADL 可以导致对命名空间 std 中同名自定义点的调用,那么提供具有强语义约束的自定义点几乎没有什么好处。例如,考虑单一类型 Swappable 概念的定义:

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));
    };
  }
}}}}
Run Code Online (Sandbox Code Playgroud)

名称交换的非限定名称查找可以直接在命名空间 std 中找到不受约束的交换——它只是命名空间层次结构上的几个跃点——或者通过 ADL 如果std是T或的关联命名空间U。如果std::swap不受约束,则该概念对所有类型都“满足”,并且实际上无用。Ranges TS 通过要求更改 std::swap 来处理这个问题,这种做法在历史上一直被禁止用于 TS。通过修改命名空间 std 中的定义,对 TS 中定义的所有定制点应用类似的约束是一个不能令人满意的解决方案,如果不是完全站不住脚的话。

Range TS 建立在 C++14 之上,其中std::swap不受约束(std::swap直到C++17 采用P0185才受到约束),因此重要的是要确保它Swappable不会对任何类型简单地解析为 true是有std作为关联的命名空间。

不过现在std::swap是被束缚住了,不需要swap毒丸了。

但是,std::iter_swap仍然不受约束,因此是需要一种毒药。然而,那个人很容易受到约束,然后我们就不再需要毒丸了。

开始/结束

如P0970 所述:

为了兼容std::begin和易于迁移,std::experimental::ranges::begin接受右值并将它们视为与 const 左值相同。此行为已被弃用,因为它从根本上是不健全的:由此类重载返回的任何迭代器很可能会在包含调用的完整表达式之后悬垂

另一个问题,直到最近才似乎与 begin 的设计无关,如果传递给它们的范围是右值,则返回迭代器的算法会将这些迭代器包装在 std::experimental::ranges::dangling<> 中。这忽略了一个事实,即对于某些范围类型——subrange<>尤其是P0789's ——迭代器的有效性根本不依赖于范围的生命周期。在将纯右subrange<>值传递给算法的情况下,完全没有必要返回包装的迭代器。

[...]

我们认识到,通过从范围访问自定义点中删除不推荐使用的对右值的默认支持,我们为范围作者提供了设计空间,以便为他们的范围类型选择这种行为,从而向算法传达迭代器可以安全地超过其范围类型。这消除了dangling传递 rvalue 时的需要,这是subrange一个重要的使用场景。

该论文接着提出了一种设计,用于begin将右值安全调用为非成员函数,特别是一个右值。的存在:

template <class T>
void begin(T&&) = delete;
Run Code Online (Sandbox Code Playgroud)

超载:

给出std2::begin的属性是,对于某些Etype 的右值表达式,除非有一个ADL 可找到的自由函数,该函数专门接受 type 的右值T,std2::begin(E)否则表达式将不会编译,并且该重载通过偏序优先于一般“毒丸”重载。beginTvoid begin(T&&)

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

但这篇论文也说:

笔者认为,解决这个问题subrange,并dangling需要增加一个新的特质给范围类型的作家的方式来表示,其迭代器是否可以安全地活得比范围。这感觉就像一个黑客,而作者无法为这样一个足够简洁明了的特征取一个名字,从而强化了这种感觉。

从那时起,这个功能就被一个特征检查了——它最初被称为enable_safe_range( P1858 ),现在被称为enable_borrowed_range( LWG3379 )。如此一来,这里的毒丸就不再需要了。