我似乎理解它们提供了一种编写自定义版本的 begin
,swap
,data
等函数的方式,这些函数通过ADL由标准库找到。这是正确的吗?
与用户在其自己的命名空间中为其类型定义 begin
的重载之前的做法有何不同?特别是,它们为什么是对象?
namespace a {
struct A {};
// Knows what to do with the argument, but doesn't check type requirements:
void customization_point(const A&);
}
// Does concept checking, then calls a::customization_point via ADL:
std::customization_point(a::A{});
目前使用例如std::swap
、std::begin
等功能还无法实现此操作。
让我尝试解释一下标准库中这一部分背后的提案。有两个问题与标准库使用的“经典”定制点相关。
They are easy to get wrong. As an example, swapping objects in generic code is supposed to look like this
template<class T> void f(T& t1, T& t2)
{
using std::swap;
swap(t1, t2);
}
but making a qualified call to std::swap(t1, t2)
instead is too simple - the user-provided
swap
would never be called (see
N4381, Motivation and Scope)
More severely, there is no way to centralize (conceptified) constraints on types passed to such user provided functions (this is also why this topic gained importance with C++20). Again from N4381:
Suppose that a future version of
std::begin
requires that its argument model a Range concept. Adding such a constraint would have no effect on code that usesstd::begin
idiomatically:
using std::begin;
begin(a);
If the call to begin dispatches to a user-defined overload, then the constraint onstd::begin
has been bypassed.
该提案所描述的解决方案通过以下方式缓解了两个问题,实现类似于std::begin
的虚构实现。
namespace std {
namespace __detail {
/* Classical definitions of function templates "begin" for
raw arrays and ranges... */
struct __begin_fn {
/* Call operator template that performs concept checking and
* invokes begin(arg). This is the heart of the technique.
* Everyting from above is already in the __detail scope, but
* ADL is triggered, too. */
};
}
/* Thanks to @cpplearner for pointing out that the global
function object will be an inline variable: */
inline constexpr __detail::__begin_fn begin{};
}
std::begin(someObject)
的合格调用始终会通过std::__detail::__begin_fn
进行绕路,这是期望的。对于未经限定的调用发生了什么,我再次参考原始论文:
这样,可以在在将
std::begin
引入作用域后对未经限定的调用begin的情况下,情况就不同了。在查找的第一阶段中,名称begin将解析为全局对象std::begin
。由于查找已经找到了一个对象而不是函数,因此不会执行查找的第二阶段。换句话说,如果std::begin
是一个对象,则using std::begin; begin(a);
等同于std::begin(a);
,如我们已经看到的那样,代表用户的依赖查找。
std
命名空间中的函数对象内执行概念检查,在执行用户提供的函数的ADL调用之前进行。没有办法规避这种情况。inline constexpr __detail::__begin_fn begin{};
即可。 - cpplearnerstd::
中直接没有CPOs。 - T.C.std::begin
这样的自定义点仍然是自由函数,而不是函数对象,对吧?唯一作为函数对象实现的自定义点是来自范围库的那些,例如 std::ranges::begin
。 - ABu“自定义点对象”这个术语有些不准确。很多(可能大多数)实际上并不是自定义点。
例如 ranges::begin
、ranges::end
和 ranges::swap
是“真正的” CPO。调用其中之一会导致一些复杂的元编程发生,以确定是否存在有效的自定义 begin
、end
或 swap
可以调用,或者是否应该使用默认实现,或者是否应该将调用作为SFINAE中的无效形式。因为许多库概念是根据CPO调用是否有效来定义的(例如 Range
和 Swappable
),因此正确约束的通用代码必须使用此类 CPO。当然,如果您知道具体类型和另一种获取其迭代器的方法,则随意使用其他方法。
例如 ranges::cbegin
是没有“CP”部分的 CPO。它们始终执行默认操作,因此没有太多的自定义空间。同样,范围适配器对象是 CPO,但它们并没有可定制化的内容。将它们归类为 CPO 更多的是为了保持一致性(对于 cbegin
)或规范上的方便(适配器)。
最后,像ranges::all_of
这样的东西是准CPO或niebloids。它们被指定为具有特殊魔术ADL阻塞属性的函数模板,并使用诡辩措辞以便可以实现为函数对象。这主要是为了防止未限制调用std::ranges
中的约束算法时,ADL选择命名空间std
中的非受约束重载。因为std::ranges
算法接受迭代器-哨兵对,通常比其std
对应项更不专业化,结果失去了重载分辨率。
ranges::data
、ranges::size
和ranges::empty
是C++20中的范围库函数,它们都是CPO(自定义操作符)的变体,并且被定义为“真正”的CPO。 - metalfox