什么是自定义点对象以及如何使用它们?

What are customization point objects and how to use them?

最后的c++标准草案引入了所谓的"customization point objects"([customization.point.object]), 被范围库广泛使用。

我似乎明白他们提供了一种编写beginswapdata等自定义版本的方法,它们是 由 ADL 的标准库找到。对吗?

这与以前用户定义重载的做法有何不同begin 适合她自己的类型 命名空间?特别是,为什么它们是 objects?

What are customization point objects?

它们是命名空间 std 中的函数对象实例,实现两个目标:首先 无条件触发(概念化的)参数类型要求,然后 分派到命名空间 std 或通过 ADL 中的正确函数。

In particular, why are they objects?

有必要绕过第二个查找阶段,该阶段会通过 ADL 直接引入用户提供的功能(这应该 推迟 按设计)。详情见下文。

... and how to use them?

开发应用程序时:您通常不会。这是一个标准库功能,它将向未来的自定义点添加概念检查,希望得到例如当您弄乱模板实例化时,会出现清晰的错误消息。但是,对这样的自定义点进行合格的调用,您就可以直接使用它。这是一个符合设计的假想 std::customization_point 对象的示例:

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::swapstd::begin等。

解释(N4381的总结)

让我试着消化一下标准中这一部分背后的提案。标准库使用的 "classical" 自定义点存在两个问题。

  • 它们很容易出错。例如,在通用代码中交换对象应该是这样的

    template<class T> void f(T& t1, T& t2)
    {
        using std::swap;
        swap(t1, t2);
    }
    

    但是对 std::swap(t1, t2) 进行合格调用太简单了 - 用户提供的 swap 永远不会被调用(见 N4381、动机和范围)

  • 更严重的是,无法集中(概念化)传递给此类用户提供函数的类型的约束(这也是此主题在 C++20 中变得重要的原因)。再次 来自 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 uses std::begin idiomatically:

    using std::begin;
    begin(a);

    If the call to begin dispatches to a user-defined overload, then the constraint on std::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, 这是需要的。调用不合格会怎样,再次参考原论文:

In the case that begin is called unqualified after bringing std::begin into scope, the situation is different. In the first phase of lookup, the name begin will resolve to the global object std::begin. Since lookup has found an object and not a function, the second phase of lookup is not performed. In other words, if std::begin is an object, then using std::begin; begin(a); is equivalent to std::begin(a); which, as we’ve already seen, does argument-dependent lookup on the users’ behalf.

这样,可以在 std 命名空间的函数对象中进行概念检查, 执行对用户提供的函数的 ADL 调用之前。没有办法绕过这个。

"Customization point object" 有点用词不当。许多 - 可能是大多数 - 实际上并不是定制点。

ranges::beginranges::endranges::swap 这样的东西是 "true" CPO。调用其中一个会导致一些复杂的元编程发生,以确定是否有有效的自定义 beginendswap 可以调用,或者是否应该使用默认实现,或者如果呼叫应该是格式错误的(以 SFINAE 友好的方式)。因为许多库概念是根据有效的 CPO 调用定义的(如 RangeSwappable),正确约束的通用代码必须使用此类 CPO。当然,如果您知道具体类型以及从中获取迭代器的另一种方法,请随意。

ranges::cbegin 这样的东西是没有 "CP" 部分的 CPO。他们总是做默认的事情,所以这不是一个定制点。同样,范围适配器对象是 CPO,但它们没有任何可定制的内容。将它们归类为 CPO 更多是为了一致性(对于 cbegin)或规范便利性(适配器)。

最后,像 ranges::all_of 这样的东西是准 CPO 或 niebloids。它们被指定为函数模板,具有特殊的神奇 ADL 阻止属性和狡猾的措辞,以允许它们作为函数对象来实现。这主要是为了防止当 std::ranges 中的受限算法被称为不合格时,ADL 在命名空间 std 中拾取不受约束的重载。因为 std::ranges 算法接受迭代器-哨兵对,所以它通常不如它的 std 算法专门化,因此失去重载解析。