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

时间:2018-11-27 08:53:49

标签: c++ c++20

c ++标准的最新草案介绍了所谓的“定制点对象”([customization.point.object]), 范围库广泛使用了

我似乎理解它们提供了一种编写beginswapdata等自定义版本的方法。 由ADL在标准库中找到。正确吗?

这与以前的做法有何不同,在以前的做法中,用户定义了例如begin代表自己的类型 命名空间?特别是为什么它们对象

2 个答案:

答案 0 :(得分:14)

  

什么是定制点对象?

它们是名称空间std中的函数对象实例,可以实现两个目标: first 无条件触发(自定义)参数的类型要求,然后然后通过名称空间std或通过ADL分配给正确的函数。

  

尤其是为什么它们是对象

规避第二个查找阶段(通过ADL直接引入用户提供的功能)是必要的(设计上应将其推迟 )。有关更多详细信息,请参见下文。

  

...以及如何使用它们?

在开发应用程序时:您主要不这样做。这是一个标准的库功能,它将为将来的自定义点添加概念检查,希望例如弄乱模板实例时,会收到清晰的错误消息。但是,通过对此类自定义点的限定调用,您可以直接使用它。这是一个符合设计的假想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的摘要)

让我尝试摘要了解本部分中的提案。标准库使用“经典”定制点有两个问题。

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

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

    ,但是对std::swap(t1, t2)进行限定调用太简单了-用户提供 swap将永远不会被调用(请参阅 N4381,动机和范围)

  • 更严重的是,没有办法集中(传递给传递给此类用户提供的函数的类型的约束)(这也是为什么该主题在C ++ 20中变得越来越重要的原因)。再次 来自N4381

      

    假设std::begin的将来版本要求其参数对Range概念建模。   添加这样的约束将不会对惯用std::begin的代码产生影响:   

      using std::begin;   
      begin(a);   

      如果开始调用分配给用户定义的重载,则对std::begin的约束   已被绕过。

提案中描述的解决方案可以缓解这两个问题 通过类似下面的方法,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调用。没有办法避免这种情况。

答案 1 :(得分:6)

“定制点对象”有点用词不当。许多(可能是多数)实际上并不是定制点。

诸如ranges::beginranges::endranges::swap之类的都是“真实” CPO。调用其中之一会导致进行一些复杂的元编程,以弄清楚是否存在要调用的有效的自定义beginendswap,或者是否应使用默认实现,或者呼叫是否格式错误(以SFINAE友好的方式)。因为根据有效的CPO调用定义了许多库概念(例如RangeSwappable),所以正确约束的通用代码必须使用此类CPO。当然,如果您知道具体的类型以及从中获得迭代器的另一种方法,请放心。

诸如ranges::cbegin之类的东西是没有“ CP”部分的CPO。他们总是做默认的事情,所以这不是定制的重点。同样,范围适配器对象是CPO,但没有可自定义的对象。将它们归类为CPO主要是为了保持一致性(为了cbegin)或规范方便(适配器)。

最后,像ranges::all_of之类的东西是准CPO或准四倍体。它们被指定为具有特殊的神奇ADL阻止属性和狡猾的措辞的功能模板,从而可以将其实现为功能对象。这主要是为了防止在std中的约束算法被称为不合格时,ADL在命名空间std::ranges中拾取不受约束的重载。因为std::range算法接受迭代器-前哨对,所以它通常不如std对应,并且失去了重载分辨率。