11

从 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;你想禁止调用复制/移动分配/ctor的上下文。

我很好奇这是否有必要,所以我= 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.
}

它不仅可以编译,而且通过调用 user defined swapfor似乎表现良好struct dummy。所以我想知道,

  1. template<typename _Tp> void swap(_Tp&, _Tp&) = delete;在这种情况下究竟做了什么?
  2. 在什么情况下没有 template<typename _Tp> void swap(_Tp&, _Tp&) = delete;?
4

2 回答 2

8

TL; DR:它在那里阻止调用std::swap

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

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

 template<class T>
  void swap(T&, T&) = delete;

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

template< class T >
void swap( T& a, T& b );

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

请注意,ranges::begin/end和类似的功能具有类似的措辞来关闭具有类似名称的std::功能的类似问题。

于 2020-08-23T01:52:03.897 回答
8

There were two motivations for the poison pill overloads, most of which don't actually exist anymore but we still have them anyway.

swap / iter_swap

As described in 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.

The Range TS was built on C++14, where std::swap was unconstrained (std::swap didn't become constrained until P0185 was adopted for C++17), so it was important to make sure that Swappable didn't just trivially resolve to true for any type that had std as an associated namespace.

But now std::swap is constrained, so there's no need for the swap poison pill.

However, std::iter_swap is still unconstrained, so there is a need for that poison pill. However, that one could easily become constrained and then we would again have no need for a poison pill.

begin / end

As described in P0970:

For the sake of compatibility with std::begin and ease of migration, std::experimental::ranges::begin accepted rvalues and treated them the same as const lvalues. This behavior was deprecated because it is fundamentally unsound: any iterator returned by such an overload is highly likely to dangle after the full expression that contained the invocation ofbegin

Another problem, and one that until recently seemed unrelated to the design of begin, was that algorithms that return iterators will wrap those iterators in std::experimental::ranges::dangling<>if the range passed to them is an rvalue. This ignores the fact that for some range types — P0789’s subrange<>, in particular — the iterator’s validity does not depend on the range’s lifetime at all. In the case where a prvalue subrange<> is passed to an algorithm, returning a wrapped iterator is totally unnecessary.

[...]

We recognized that by removing the deprecated default support for rvalues from the range access customization points, we made design space for range authors to opt-in to this behavior for their range types, thereby communicating to the algorithms that an iterator can safely outlive its range type. This eliminates the need for dangling when passing an rvalue subrange, an important usage scenario.

The paper went on to propose a design for safe invocation of begin on rvalues as a non-member function that takes, specifically, an rvalue. The existence of the:

template <class T>
void begin(T&&) = delete;

overload:

gives std2::begin the property that, for some rvalue expression E of type T, the expression std2::begin(E) will not compile unless there is a free function begin findable by ADL that specifically accepts rvalues of type T, and that overload is prefered by partial ordering over the general void begin(T&&) “poison pill” overload.

For example, this would allow us to properly reject invoking ranges::begin on an rvalue of type std::vector<int>, even though the non-member std::begin(const C&) would be found by ADL.

But this paper also says:

The author believed that to fix the problem with subrange and dangling would require the addition of a new trait to give the authors of range types a way to say whether its iterators can safely outlive the range. That felt like a hack, and that feeling was reinforced by the author’s inability to pick a name for such a trait that was sufficiently succint and clear.

Since then, this functionality has become checked by a trait - which was first called enable_safe_range (P1858) and is now called enable_borrowed_range (LWG3379). So again, the poison pill here is no longer necessary.

于 2020-08-23T14:39:16.350 回答