为什么在定义自定义点对象时删除功能是必要的? 交换/ iter_swap 开始/结束

问题描述

来自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;的情况下此中断什么时候出现?

解决方法

TL; DR:可以避免调用std::swap

这实际上是对the ranges::swap customization point的明确要求:

如果S(void)swap(E1,E2)具有类或枚举类型([basic.compound])并且该表达式有效,并且在包含此定义的上下文:
E1

那这是做什么的?要了解这一点,我们必须记住E2名称空间实际上是 template<class T> void swap(T&,T&) = delete; 名称空间。这很重要,因为ranges命名空间中包含许多内容。包括在std::ranges中声明的内容:

std

某处可能有<utility>template< class T > void swap( T& a,T& b ); ,但这与我们的需求无关。

constexpr作为自定义点,具有希望您对其进行自定义的特定方式。它希望您提供一个noexcept函数,可以通过依赖于参数的查找找到该函数。这意味着std::ranges::swap将通过执行以下操作来找到交换功能:swap

那很好,除了一个问题:ranges::swap存在。在C ++之前的20天里,使类型可交换的一种有效方法是为swap(E1,E2)模板提供特殊化。因此,如果您直接致电std::swap交换某些东西,那么您的专业将被使用。

std::swap不想使用它们。它具有一种自定义机制,并且希望您非常肯定地使用该机制,而不是std::swap的模板专业化。

但是,由于ranges::swap位于std::swap命名空间中,因此对std::ranges::swap的不合格调用可以找到std。为避免发现和使用此重载,它通过使swap(E1,E2) d版本可见而毒化了重载。因此,如果您不为您的类型提供ADL可见的std::swap,则会遇到硬错误。与= delete版本相比,适当的自定义还需要更专业(或更受约束),以便可以将其视为更好的重载匹配。

请注意,swap和类似函数的措辞相似,可以解决名称相似的std::swap函数的类似问题。

,

毒药超载的动机有两种,其中大多数实际上已经不存在了,但无论如何我们仍然有。

交换/ iter_swap

P0370中所述:

Ranges TS还有N4381不能解决的另一个定制点问题:Range 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));
    };
  }
}}}}

名称交换的不合格名称查找可以直接在命名空间std中找到不受约束的交换-只是几次跳到命名空间层次结构中-如果std是{{1}的关联命名空间,则可以通过ADL }或T。如果U不受限制,则该概念对于所有类型都是“满意的”,并且实际上是无用的。 Ranges TS通过要求更改std :: swap来解决此问题,这是TS历来禁止的做法。通过修改命名空间std中的定义,将类似的约束应用于TS中定义的所有自定义点,这不是一个令人满意的解决方案。

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

但是现在std被限制了,所以不需要std::swap毒丸了。

但是,swap仍然不受限制,因此有必要使用该毒丸。 但是,这样很容易受到约束,然后我们再也不需要毒药了。

开始/结束

P0970中所述:

为了与std::iter_swap兼容并易于迁移,std::begin接受了右值并将它们与const左值相同。不建议使用此行为,因为它从根本上讲是不合理的:由此类重载返回的任何迭代器在包含调用begin的完整表达式之后很可能会悬空

另一个问题(直到最近才似乎与begin的设计无关)是,如果传递给它们的范围是一个返回值,则返回迭代器的算法会将这些迭代器包装在std :: experimental :: ranges :: dangling 中。右值。这忽略了以下事实:对于某些范围类型(​​尤其是P0789的std::experimental::ranges::begin),迭代器的有效性完全不取决于范围的生存期。如果将prvalue subrange<>传递给算法,则完全不需要返回包装的迭代器。

[...]

我们认识到,通过从范围访问自定义点中删除不推荐使用的默认值对rvalue的支持,我们为范围作者提供了设计空间来选择其行为范围类型的行为,从而与迭代器可以安全地进行算法通信超出其范围类型。这样就可以避免在传递重要值subrange<>的情况下使用dangling

本文接着提出了一种设计,用于安全地调用rvalues上的subrange作为一个非成员函数,具体来说,它是一个rvalue。存在的

begin

超载:

赋予template <class T> void begin(T&&) = delete; 属性,对于类型为std2::begin的某些右值表达式E,除非有自由函数{{1},否则表达式T不会编译。 }可被ADL查找,该ADL专门接受类型为std2::begin(E)的右值,并且与一般的begin“毒丸”重载相比,通过部分排序更喜欢重载。

例如,这将使我们能够正确拒绝对类型为T的右值调用void begin(T&&),即使ADL将找到非成员ranges::begin

但是这篇论文还说:

作者认为,要解决std::vector<int>std::begin(const C&)的问题,就需要添加新的特征,以使范围类型的作者可以说出其迭代器是否可以安全地超出范围。这感觉像是一种骇人听闻的感觉,而作者无法为如此简洁明了的特征选择一个名字,进一步增强了这种感觉。

从那时起,此功能已通过特征进行检查-首先被称为subrangeP1858),现在被称为danglingLWG3379)。再次重申,这里不再需要毒药。