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

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

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

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

Ответ 1

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

Это экземпляры функциональных объектов в пространстве имен 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, Мотивация и область действия)

  • Точнее, нет способа централизовать (концептуализировать) ограничения на типы, передаваемые таким пользовательским функциям (именно поэтому эта тема приобрела важность в С++ 20). Снова из N4381:

    Предположим, что будущая версия std::begin требует, чтобы ее аргумент моделировал концепцию Range. Добавление такого ограничения не повлияет на код, который использует std::begin идиоматически:

    using std::begin;
    begin(a);

    Если вызов begin отправляет пользовательскую перегрузку, то ограничение на 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::begin(someObject) через std::__detail::__begin_fn, что желательно. Что происходит с неквалифицированным звонком, я снова ссылаюсь на оригинальную статью:

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

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

Ответ 2

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

Такие вещи, как ranges::begin, ranges::end и range ranges::swap являются "истинными" CPO. Вызов одного из них приводит к некоторому сложному метапрограммированию, чтобы выяснить, существует ли допустимый настраиваемый begin или end или swap для вызова, или если должна использоваться реализация по умолчанию, или если вызов должен быть неправильно сформирован (в СФИНАЕ-дружественная манера). Поскольку ряд библиотечных концепций определен в терминах допустимых вызовов CPO (например, Range и Swappable), правильно ограниченный универсальный код должен использовать такие CPO. Конечно, если вы знаете конкретный тип и другой способ получить из него итератор, не стесняйтесь.

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

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