Что такое объекты точек настройки и как их использовать?

В последнем черновике стандарта C++ представлены так называемые «объекты точки настройки» ([customization .point.object]), которые широко используются библиотекой диапазонов.

Кажется, я понимаю, что они предоставляют способ написания пользовательских версий begin, swap, data и т.п., которые находятся в стандартной библиотеке ADL. Это правильно?

Чем это отличается от предыдущей практики, когда пользователь определяет перегрузку, например. begin для ее типа в ее собственном пространстве имен? В частности, почему они являются объектами?


person metalfox    schedule 27.11.2018    source источник
comment
open-std.org/jtc1/sc22/ wg21/docs/papers/2015/n4381.html   -  person cpplearner    schedule 27.11.2018


Ответы (2)


Что такое объекты точек настройки?

Это экземпляры объектов-функций в пространстве имен std, которые выполняют две задачи: сначала безоговорочно инициируют (концептуальные) требования к типу для аргумента(ов), затем направляют в правильную функцию в пространстве имен. 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::swap, std::begin и тому подобное.

Объяснение (краткое изложение N4381)

Позвольте мне попытаться переварить предложение, стоящее за этим разделом стандарта. Есть две проблемы с «классическими» точками настройки, используемыми стандартной библиотекой.

  • Их легко ошибиться. Например, обмен объектами в универсальном коде должен выглядеть следующим образом.

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

    но вместо этого сделать квалифицированный вызов std::swap(t1, t2) слишком просто — предоставленный пользователем swap никогда не будет вызван (см. N4381, Мотивация и область применения)

  • #P11# <блочная цитата> #P12#

Решение, описанное в предложении, смягчает обе проблемы с помощью подхода, подобного следующему, воображаемой реализации 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, что желательно. О том, что происходит с неквалифицированным вызовом, я снова ссылаюсь на исходную статью:

В случае, когда begin вызывается unqualified после включения std::begin в область действия, ситуация иная. На первом этапе поиска имя begin преобразуется в глобальный объект std::begin. Поскольку поиск нашел объект, а не функцию, второй этап поиска не выполняется. Другими словами, если std::begin является объектом, то using std::begin; begin(a); эквивалентно std::begin(a);, которое, как мы уже видели, выполняет поиск в зависимости от аргумента от имени пользователя.

Таким образом, проверка концепций может выполняться внутри функционального объекта в пространстве имен std, перед выполнением ADL-вызова предоставленной пользователем функции. Обойти это невозможно.

person lubgr    schedule 27.11.2018
comment
Разве это не должно быть constexpr auto begin = __detail::__begin_fn{} в анонимном пространстве имен? - person metalfox; 27.11.2018
comment
Обратите внимание, что хитрость ODR становится спорной из-за встроенных переменных C++17. Теперь inline constexpr __detail::__begin_fn begin{}; должно быть достаточно. - person cpplearner; 27.11.2018
comment
Является ли это деталью реализации стандартной библиотеки или пользователь должен позаботиться о точках настройки? - person Oliv; 27.11.2018
comment
@Oliv Если я правильно понимаю, нет необходимости заботиться о точках настройки, кроме предоставления соответствующей перегрузки функции с тем же именем. Так же, как это было, например, для swap. - person lubgr; 27.11.2018
comment
Набросок Эрика Ниблера. У него есть отличный пост в блоге о точках настройки здесь: ericniebler.com/2014/10/21/ - person AndyG; 27.11.2018
comment
Непосредственно в std:: нет CPO, IIRC. - person T.C.; 28.11.2018
comment
Если мы делаем все возможное, вы также должны обернуть объект begin во встроенное пространство имен. - person Barry; 16.12.2018
comment
Где в стандарте содержится требование о том, что CPO должен быть одинаково во всех TU? Или это реликт от их концепции с рендж-тсом? - person Deduplicator; 26.06.2019
comment
@Deduplicator Не уверен, понимаю ли я, что вы имеете в виду, где в ответе ссылка на такое требование? - person lubgr; 27.06.2019
comment
Если я не ошибаюсь, точки настройки, такие как std::begin, по-прежнему являются бесплатными функциями, а не функциональными объектами, как для С++ 20? Единственные точки настройки, реализованные как объекты функций, — это точки из библиотеки диапазонов, такие как std::ranges::begin. - person Peregring-lk; 23.07.2019
comment
@ Peregring-lk Я тоже так думаю, иначе это нарушило бы обратную совместимость. - person lubgr; 23.07.2019
comment
@lubgr Спасибо. Это было под сомнением, потому что std::begin — это наиболее распространенный пример, используемый для объяснения объектов точки настройки, хотя на самом деле это не реализовано таким образом. - person Peregring-lk; 24.07.2019
comment
Сбивает с толку. Я попал на эту страницу, потому что думал о растущей тенденции в стандарте C++ работать с точками настройки, которые определяются классами в целом, а не только теми, которые удовлетворяют концепции Callable. Например. Coroutines TS в значительной степени зависит от нескольких определяемых классов. Но здесь было указано, что CPO являются функциональными объектами. Я надеюсь, что это неправильно, потому что если это так, это будет еще одна ужасная номенклатура, которая сбивает с толку. Мне кажется, что все они CPO, вне зависимости от того, есть ли у них operator(). Иначе что вы называете неотзывными? - person Daniel Russell; 05.03.2020

«Объект точки настройки» — это неправильное название. Многие — вероятно, большинство — на самом деле не являются точками настройки.

Такие вещи, как ranges::begin, ranges::end и ranges::swap, являются «настоящими» CPO. Вызов одного из них приводит к сложному метапрограммированию, чтобы выяснить, существует ли допустимый настроенный begin, end или swap для вызова, или следует ли использовать реализацию по умолчанию, или вместо этого вызов должен быть неправильно сформирован (в SFINAE-дружелюбная манера). Поскольку ряд понятий библиотеки определяется с точки зрения допустимости вызовов CPO (например, Range и Swappable), правильно ограниченный универсальный код должен использовать такие CPO. Конечно, если вы знаете конкретный тип и другой способ получить из него итератор, не стесняйтесь.

Такие вещи, как ranges::cbegin, являются CPO без части "CP". Они всегда делают что-то по умолчанию, так что это не очень важно для настройки. Точно так же объекты адаптера диапазона являются объектами CPO, но в них нет ничего настраиваемого. Классификация их как CPO является скорее вопросом согласованности (для cbegin) или удобства спецификации (адаптеры).

Наконец, такие вещи, как ranges::all_of, являются квази-CPO или ниблоидами. Они указаны как шаблоны функций со специальными волшебными свойствами блокировки ADL и ласковой формулировкой, позволяющей вместо этого реализовывать их как объекты функций. В первую очередь это делается для того, чтобы ADL не вызывал неограниченную перегрузку в пространстве имен std, когда алгоритм с ограничениями в std::ranges вызывается неквалифицированным. Поскольку алгоритм std::ranges принимает пары итератор-страж, он обычно менее специализирован, чем его аналог std, и в результате теряет разрешение перегрузки.

person T.C.    schedule 28.11.2018
comment
А как насчет ranges::data, ranges::size и ranges::empty? Являются ли они настоящим CPO? - person metalfox; 28.11.2018
comment
Да, они действительно настраиваемые. - person T.C.; 29.11.2018