为什么在定义自定义点 object 时必须删除函数?
Why is deleting a function necessary when you're defining customization point object?
来自 libstdc++ <concepts>
header:
namespace ranges
{
namespace __cust_swap
{
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
来自 MS-STL <concepts>
header:
namespace ranges {
namespace _Swap {
template <class _Ty>
void swap(_Ty&, _Ty&) = delete;
我从未遇到过 = delete;
在您要禁止调用 copy/move assignment/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.
}
不仅可以编译,而且通过为 struct dummy
调用用户定义的 swap
似乎表现良好。所以我想知道,
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
在这种情况下到底做了什么?
- 没有
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
会在什么情况下中断?
TL;DR: 它是用来防止调用 std::swap
.
这其实是the ranges::swap
customization point的明确要求:
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 );
那里可能有 constexpr
和 noexcept
,但这与我们的需求无关。
std::ranges::swap
,作为一个定制点,它有一个特定的方式让你定制它。它希望您提供可以通过 argument-dependent 查找找到的 swap
函数。这意味着 ranges::swap
将通过以下方式找到您的交换函数:swap(E1, E2)
.
很好,除了一个问题:std::swap
存在。在 pre-C++20 天里,使类型可交换的一种有效方法是为 std::swap
模板提供特化。因此,如果您直接调用 std::swap
来交换某些东西,您的专长将被选取并使用。
ranges::swap
不 想要使用那些。它有一种定制机制,它希望您非常肯定地使用该机制,而不是 std::swap
.
的模板专业化
但是,因为 std::ranges::swap
位于 std
命名空间中,所以对 swap(E1, E2)
的非限定调用可以找到 std::swap
。为了避免发现和使用此重载,它通过使 = delete
d 版本可见来中毒重载。因此,如果您没有为您的类型提供 ADL-visible swap
,则会出现严重错误。还需要进行适当的定制,使其比 std::swap
版本更专业(或更受约束),以便它可以被认为是更好的重载匹配。
请注意,ranges::begin/end
和类似函数具有类似的措辞,以关闭具有类似名称的 std::
函数的类似问题。
毒丸超载有两个动机,其中大部分实际上已不存在,但无论如何我们仍然有。
交换/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所述:
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.
该论文接着提出了一种安全调用 begin
右值的设计,作为一个 non-member 函数,特别是接受一个右值。存在的:
template <class T>
void begin(T&&) = delete;
过载:
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.
例如,这将允许我们正确拒绝对 std::vector<int>
类型的右值调用 ranges::begin
,即使 ADL 会找到 non-member std::begin(const C&)
.
但是这篇论文还说:
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.
从那时起,此功能已通过特征进行检查 - 首先称为 enable_safe_range
(P1858) and is now called enable_borrowed_range
(LWG3379)。所以还是那句话,这里的毒丸就不用了
来自 libstdc++ <concepts>
header:
namespace ranges
{
namespace __cust_swap
{
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
来自 MS-STL <concepts>
header:
namespace ranges {
namespace _Swap {
template <class _Ty>
void swap(_Ty&, _Ty&) = delete;
我从未遇到过 = delete;
在您要禁止调用 copy/move assignment/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.
}
不仅可以编译,而且通过为 struct dummy
调用用户定义的 swap
似乎表现良好。所以我想知道,
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
在这种情况下到底做了什么?- 没有
template<typename _Tp> void swap(_Tp&, _Tp&) = delete;
会在什么情况下中断?
TL;DR: 它是用来防止调用 std::swap
.
这其实是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;
那么这是做什么的呢?要理解这一点,我们必须记住 ranges
命名空间实际上是 std::ranges
命名空间。这很重要,因为很多东西都存在于 std
命名空间中。包括这个,在 <utility>
:
template< class T >
void swap( T& a, T& b );
那里可能有 constexpr
和 noexcept
,但这与我们的需求无关。
std::ranges::swap
,作为一个定制点,它有一个特定的方式让你定制它。它希望您提供可以通过 argument-dependent 查找找到的 swap
函数。这意味着 ranges::swap
将通过以下方式找到您的交换函数:swap(E1, E2)
.
很好,除了一个问题:std::swap
存在。在 pre-C++20 天里,使类型可交换的一种有效方法是为 std::swap
模板提供特化。因此,如果您直接调用 std::swap
来交换某些东西,您的专长将被选取并使用。
ranges::swap
不 想要使用那些。它有一种定制机制,它希望您非常肯定地使用该机制,而不是 std::swap
.
但是,因为 std::ranges::swap
位于 std
命名空间中,所以对 swap(E1, E2)
的非限定调用可以找到 std::swap
。为了避免发现和使用此重载,它通过使 = delete
d 版本可见来中毒重载。因此,如果您没有为您的类型提供 ADL-visible swap
,则会出现严重错误。还需要进行适当的定制,使其比 std::swap
版本更专业(或更受约束),以便它可以被认为是更好的重载匹配。
请注意,ranges::begin/end
和类似函数具有类似的措辞,以关闭具有类似名称的 std::
函数的类似问题。
毒丸超载有两个动机,其中大部分实际上已不存在,但无论如何我们仍然有。
交换/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 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.
Range TS 基于 C++14 构建,其中 std::swap
不受约束(std::swap
在 C++17 采用 P0185 之前没有受到约束),因此,重要的是要确保 Swappable
不会简单地将 std
作为关联命名空间的任何类型解析为 true。
但是现在std::swap
被约束了,所以swap
毒丸就没必要了
然而,std::iter_swap
仍然不受约束,因此需要那个毒丸。 但是,那个人很容易被束缚,然后我们又不需要毒丸了。
开始/结束
如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.
该论文接着提出了一种安全调用 begin
右值的设计,作为一个 non-member 函数,特别是接受一个右值。存在的:
template <class T>
void begin(T&&) = delete;
过载:
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.
例如,这将允许我们正确拒绝对 std::vector<int>
类型的右值调用 ranges::begin
,即使 ADL 会找到 non-member std::begin(const C&)
.
但是这篇论文还说:
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.
从那时起,此功能已通过特征进行检查 - 首先称为 enable_safe_range
(P1858) and is now called enable_borrowed_range
(LWG3379)。所以还是那句话,这里的毒丸就不用了