From libstdc++ <concepts>
header:
namespace ranges
{
namespace __cust_swap
{
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
From MS-STL <concepts>
header:
namespace ranges {
namespace _Swap {
template <class _Ty>
void swap(_Ty&, _Ty&) = delete;
I've never encountered = delete;
outside the context where you want to prohibit the call to copy/move assignment/ctor.
I was curious if this was necessary, so I've commented out the = delete;
part from the library like this:
// template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
to see if the following test case compiles.
#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.
}
Not only it compiles, it seems to behave well by calling user defined swap
for struct dummy
. So I'm wondering,
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
exactly do in this context? template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
?TL;DR: It's there to keep from calling std::swap
.
This is actually an explicit requirement of the ranges::swap
customization point:
S
is(void)swap(E1, E2)
ifE1
orE2
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;
So what does this do? To understand the point of this, we have to remember that the ranges
namespace is actually the std::ranges
namespace. That's important because a lot of stuff lives in the std
namespace. Including this, declared in <utility>
:
template< class T >
void swap( T& a, T& b );
There's probably a constexpr
and noexcept
on there somewhere, but that's not relevant for our needs.
std::ranges::swap
, as a customization point, has a specific way it wants you to customize it. It wants you to provide a swap
function that can be found via argument-dependent lookup. Which means that ranges::swap
is going to find your swap function by doing this: swap(E1, E2)
.
That's fine, except for one problem: std::swap
exists. In the pre-C++20 days, one valid way of making a type swappable was to provide a specialization for the std::swap
template. So if you called std::swap
directly to swap something, your specializations would be picked up and used.
ranges::swap
does not want to use those. It has one customization mechanism, and it wants you to very definitely use that mechanism, not template specialization of std::swap
.
However, because std::ranges::swap
lives in the std
namespace, unqualified calls to swap(E1, E2)
can find std::swap
. To avoid finding and using this overload, it poisons the overload by making visible a version that is = delete
d. So if you don't provide an ADL-visible swap
for your type, you get a hard error. A proper customization is also required to be more specialized (or more constrained) than the std::swap
version, so that it can be considered a better overload match.
Note that ranges::begin/end
and similar functions have similar wording to shut down similar problems with similarly named std::
functions.
There were two motivations for the poison pill overloads, most of which don't actually exist anymore but we still have them anyway.
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 ofT
orU
. Ifstd::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.
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 ofbeginAnother 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 prvaluesubrange<>
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 rvaluesubrange
, 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 expressionE
of typeT
, the expressionstd2::begin(E)
will not compile unless there is a free functionbegin
findable by ADL that specifically accepts rvalues of typeT
, and that overload is prefered by partial ordering over the generalvoid 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
anddangling
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With