Подтвердить что ты не робот

Могут ли перегрузки для общих функций быть открытыми для других перегрузок?

Я хочу реализовать некоторые общие алгоритмы, и у меня есть ряд идей о том, как можно реализовать специализированные алгоритмы в зависимости от определенных признаков сущностей, с которыми используется алгоритм. Тем не менее, кажется вероятным, что я не придумал все специальные черты, и я хотел бы реализовать общую версию, чтобы они могли работать с другой специализированной версией.

Например, рассмотрим distance(begin, end) (да, я знаю, что он находится в библиотеке standad, однако он приятный и простой и может быть использован для демонстрации моей проблемы). Общая версия может выглядеть так (я использую std::ptrdiff_t вместо std::iterator_traits<It>::difference_type как другое упрощение):

template <typename It>
auto distance(It it, It end) -> std::ptrdiff_t {
    std::ptrdiff_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

Конечно, если тип итератора является итератором с произвольным доступом, гораздо лучше реализовать алгоритм, используя разницу между двумя итераторами. Наивно просто добавляя

template <typename It>
auto distance(It begin, It end)
     -> typename std::enable_if<is_random_access_v<It>, std::ptrdiff_t>::type {
    return end - begin;
}

не совсем работает: обе реализации одинаково хороши для итераторов с произвольным доступом, т.е. компилятор считает их неоднозначными. Легкий подход к решению этой проблемы состоит в том, чтобы изменить общую реализацию, применимую только для итераторов без случайного доступа. То есть, выбор SFINAE сделан таким образом, что они являются взаимоисключающими, а также охватывают все пространство.

К сожалению, набор реализаций по-прежнему закрыт: без изменения подписи, по крайней мере, одной из реализаций я не могу добавить другую реализацию, если у меня есть другая идея для универсальной реализации, использующей специальные свойства. Например, если я хочу добавить специальную обработку для сегментированных диапазонов (идея: когда базовая последовательность состоит из сегментов как есть, например, для случая std::deque<...> или std::istreambuf_iterator<cT>, обрабатывать сегменты отдельно), необходимо будет изменить общая реализация должна применяться только тогда, когда последовательности не являются произвольным доступом и не являются сегментированной последовательностью. Конечно, если я контролирую выполнение, которое можно сделать. Пользователь не сможет расширять набор общих реализаций.

Я знаю, что функции могут быть перегружены для специальных типов итераторов. Однако для этого потребуется, чтобы каждый раз, когда добавлялся итератор со специальными возможностями, ему необходимо было реализовать соответствующие функции. Цель состоит в том, чтобы включить добавление универсальных реализаций, которые являются улучшением в случае, если объекты, которые они используют, предоставляют дополнительные возможности. Он похож на разные категории итераторов, хотя свойства ортогональны категориям итераторов.

Таким образом, мой вопрос:

  • Можно ли реализовать универсальные алгоритмы, чтобы можно было добавить новую идею улучшения без изменения существующих реализаций, и если да, то как?
  • Дополнительное наблюдение (меня в первую очередь интересует вопрос выше, но это тоже может быть интересно): Если это невозможно, добавилась бы эта способность с концепциями?
4b9b3361

Ответ 1

Одним из подходов является механизм перегрузки, основанный на ранжировании. Назначьте каждую перегрузку ранга и разрешите перегрузочное разрешение сделать все остальное.
Это вспомогательные черты:

template <unsigned i> struct rank : rank<i - 1> {};

template <> struct rank<0> {};

using call_ranked = rank<256>;

И это пример использования:

template <typename It>
auto distance_ranked(rank<0>, It it, It end) -> std::size_t {
    std::size_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

template <typename It>
auto distance_ranked(rank<1>, It begin, It end)
     -> typename std::enable_if<is_random_access_v<It>, std::size_t>::type {
    return end - begin;
}

// Delegating function template:
template <typename... Args>
auto distance(Args&&... args)
    -> decltype(distance_ranked(call_ranked(), std::forward<Args>(args)...)) {
    return      distance_ranked(call_ranked(), std::forward<Args>(args)...);
}

Демо.
Ранг с более высоким числом более приоритетным, чем с более низким числом. То есть rank<1> вызывает выбор второй перегрузки по первой (rank<0>), если совпадения в противном случае были бы идентичными.

Если вы хотите добавить реализацию на основе сегмента, используйте это как условие для enable_if. Предположительно, сегментированные диапазоны и диапазоны произвольного доступа будут взаимоисключающими, но если они не являются, назначьте произвольный доступ более высоким приоритетом. Общее руководство может быть: чем эффективнее реализация, тем выше ее рейтинг.
Используя этот метод, другие внедрения не должны затрагиваться при введении нового. Нужно убедиться, что любые две категории с непустыми пересечениями (которые не охватываются категорией с более высоким рангом) имеют разный ранг, что представляет собой заметный недостаток.

Ответ 2

Концепции предпочитает более ограниченную перегрузку по сравнению с менее сложной перегрузкой, поэтому вам не нужно исключать область ограниченной реализации из области неограниченной реализации как вы бы с SFINAE. Ваша основная реализация может быть записана как:

template <typename It>
std::size_t distance(It it, It end) {
    std::size_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

template <typename It>
requires is_random_access_v<It>
std::size_t distance(It begin, It end) {
    return end - begin;
}

нет необходимости исключать итераторы произвольного доступа (область ограниченной перегрузки) из домена без ограничений.

Если все сегментированные итераторы являются случайными или все случайные итераторы сегментируются, то снова Concepts предпочитает более ограниченную перегрузку, и все в порядке. Вы просто добавляете новую ограниченную перегрузку:

template <typename It>
requires SegmentedIterator<It>
std::size_t distance(It begin, It end) {
    // ...
}

Если у вас ограниченные перегрузки с перекрывающимися диапазонами, но ни одно из них не связано с ограничениями другого, разрешение перегрузки неоднозначно, как и для SFINAE. Однако нарушение двусмысленности немного проще, так как только необходимо добавить новую перегрузку, чтобы указать поведение в области перекрытия:

template <typename It>
requires SegmentedIterator<It> && is_random_access_v<It>
std::size_t distance(It begin, It end) {
    // ...
}

SFINAE потребует от вас дополнительного исключения перекрытия из домена других перегрузок, но Concepts предпочтет эту более ограниченную перегрузку, не требуя изменений перегрузок для SegmentedIterator и is_random_access_v.

Концепции позволяют пользователю легко расширять общую реализацию с помощью ортогональных перегрузок. Неортогональные перегрузки требуют больше усилий для указания поведения в "перекрытии", но не требуют изменений исходного кода, как SFINAE.

Ответ 3

Обратите внимание, что вы можете "подражать" концепциям на С++ 11, используя трюк Walter Brown void_t (см. void_t" может реализовывать концепции "?).

Затем вы можете предоставить базовую реализацию в качестве шаблона класса

template <typename It, class=void>
struct dist_impl {
auto operator()(It it, It end) -> std::size_t {
    std::size_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    cout << "base distance\n";
    return size;
}
};

и выполните частичную специализацию с void_t, чтобы компилятор выбрал наиболее специализированное соответствие

template <typename It>
struct dist_impl<It, void_t<typename std::enable_if<is_random_access<It>::value>::type>> {
auto operator()(It begin, It end) -> std::size_t {
    cout << "random distance\n";
    return end - begin;
}
};

Используются те же "ортогональные" соображения.

Вот полный пример: http://coliru.stacked-crooked.com/a/e4fd8d6860119d42

Ответ 4

Перегрузки

Во-первых, давайте взглянем на простое управление функцией distance. Используя библиотеку Tick, вы можете реализовать концептуальные черты для обходов итератора в С++, например:

TICK_TRAIT(is_incrementable)
{
    template<class T>
    auto requires_(T&& x) -> tick::valid<
        decltype(x++),
        decltype(++x)
    >;
};

TICK_TRAIT(is_decrementable, is_incrementable<_>)
{
    template<class T>
    auto requires_(T&& x) -> tick::valid<
        decltype(x--),
        decltype(--x)
    >;
};

TICK_TRAIT(is_advanceable)
{
    template<class T, class Number>
    auto requires_(T&& x, Number n) -> tick::valid<
        decltype(x += n)
    >;
};

Теперь, если вы напишете две перегрузки, это может быть неоднозначным. Таким образом, существует несколько способов устранения двусмысленности. Во-первых, вы можете использовать диспетчер отправки тегов:

template <typename It>
auto distance(It it, It end, tick::tag<is_incrementable>) -> std::ptrdiff_t 
{
    std::ptrdiff_t size{};
    while (it != end) {
        ++it;
        ++size;
    }
    return size;
}

template <typename It>
auto distance(It begin, It end, tick::tag<is_advanceable>())
{
    return end - begin;
}

template<typename It, TICK_REQUIRES(is_incrementable<It>())>
auto distance(It begin, It end)
{
    return distance(begin, end, tick::most_refined<is_advanceable<It>());
}

Другим способом является использование условной перегрузки, предоставляемой библиотекой Fit. Это позволяет вашему заказу выполнять функции по важности, чтобы избежать двусмысленности. Вы можете использовать объекты функций или лямбда. Вот как это сделать, используя общие лямбды:

FIT_STATIC_FUNCTION(distance) = fit::conditional(
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_incrementable>(begin) and 
        tick::trait<is_incrementable>(end)))
    {
        std::ptrdiff_t size{};
        while (it != end) {
            ++it;
            ++size;
        }
        return size;
    },
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_advanceable>(begin) and 
        tick::trait<is_advanceable>(end)))
    {
        return end - begin;
    }
);

Конечно, это делает его объектом функции, который вам придется обернуть в фактическую функцию, если вы хотите полагаться на поиск ADL.

Точки настройки

Можно ли реализовать универсальные алгоритмы, чтобы можно было добавить новую идею улучшения без изменения существующих реализаций, и если да, то как?

Да, они могут, но вам нужно определить точки настройки.

Поиск ADL

Один из способов - поиск ADL. Функции std::begin и std::end работают таким образом. Таким образом, вы можете определить функцию distance в своем собственном пространстве имен:

namespace detail {
    template<typename It, TICK_REQUIRES(is_incrementable<It>())>
    auto distance(It begin, It end)
    {
        // Implementation of distance
    }
}

Затем вы можете определить другую функцию, которую пользователь может использовать в другом пространстве имен, например:

namespace my_lib {
template<typename It, TICK_REQUIRES(is_incrementable<It>())>
auto distance(It begin, It end)
{
    using detail::distance;
    distance(begin, end);
}
}

Итак, теперь вы можете настроить функцию distance для определенных типов.

Специализация шаблона

Однако ADL может быть непреднамеренно захвачен, и это может привести к тому, что это может иногда не срабатывать. Таким образом, другой способ предоставления точек настройки - использовать специализированную специализацию. Таким образом, вы можете определить шаблон, который может использоваться для переопределения поведения distance, например:

template<class It, class=void>
struct distance_op;

Итак, функция distance может быть определена, чтобы сначала выбрать distance_op:

 FIT_STATIC_FUNCTION(distance) = fit::conditional(
    [](auto begin, auto end) FIT_RETURNS
    (distance_op<decltype(begin)>::call(begin, end)),
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_incrementable>(begin) and 
        tick::trait<is_incrementable>(end)))
    {
        std::ptrdiff_t size{};
        while (it != end) {
            ++it;
            ++size;
        }
        return size;
    },
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_advanceable>(begin) and 
        tick::trait<is_advanceable>(end)))
    {
        return end - begin;
    }
);

FIT_RETURNS будет ограничивать лямбда, когда distance_op<decltype(begin)>::call(begin, end) действителен. Поэтому, если вы хотите настроить distance для std::queue, вы можете написать:

template<>
struct distance_op<queue<int>::iterator>
{
    static void call(queue<int>::iterator begin, queue<int>::iterator end)
    {
        // Do queue-based distance
    }
};

Кроме того, существует второй параметр, чтобы вы могли специализировать его на основе типов, соответствующих определенным ограничениям, поэтому мы могли бы реализовать его для каждого iteratar, где is_queue_iterator - true, например:

template<Iterator>
struct distance_op<Iterator, TICK_CLASS_REQUIRES(is_queue_iterator<Iterator>())>
{
    static void call(queue<int>::iterator begin, queue<int>::iterator end)
    {
        // Do queue-based distance
    }
};

Концептуальные карты

Дополнительное наблюдение (меня в первую очередь интересует вопрос выше, но это тоже может быть интересно): Если это невозможно, добавилась бы эта способность с концепциями?

Да, используя концептуальные карты, вы можете легко расширить эти операции. Таким образом, вы можете создать расстояние concept следующим образом:

template<class Iterator>
concept Distance
{
    ptrdiff_t distance(Iterator begin, Iterator end);
}

Тогда мы создадим a concept_map, когда это a Incrementable и когда оно Advanceable:

template<Incrementable Iterator>
concept_map Distance<Iterator>
{
    ptrdiff_t distance(Iterator begin, Iterator end)
    {
        std::ptrdiff_t size{};
        while (it != end) {
            ++it;
            ++size;
        }
        return size;
    }
};

template<Advanceable Iterator>
concept_map Distance<Iterator>
{
    ptrdiff_t distance(Iterator begin, Iterator end)
    {
        return end - begin;
    }
};

И затем позже пользователь может специализировать concept_map для новых типов:

template<class T>
concept_map Distance<queue<T>::iterator>
{
    ptrdiff_t distance(Iterator begin, Iterator end)
    {
        return end - begin;
    }
};

Ответ 5

Я бы использовал для этого класс утилиты, потому что в этом случае легко дать ему алгоритм по умолчанию (для общего случая), позволяющий ему переопределить его для конкретных целей. Более или менее то, что классы из STL делают с Allocator:

template < class T, class Alloc = allocator<T> > class list;

По умолчанию вы получаете allocator<T>, но можете предоставить вам собственную реализацию.

template <class T, class Dist = dist<T> >
class dist_measurer {
public:
    static auto distance(T begin, T end) {
        return Dist.distance(begin, end);
    }
}

Затем вы создаете общий dist<T> и необязательно другие конкретные реализации, все с одним единственным расстоянием статического метода.

Если вы хотите использовать общий метод в классе X:

dist_measurer<X>.distance(x, y); // x and y objects of class X

Если вы выполнили другой алгоритм в dist2, вы используете его с помощью:

dist_measurer<X, dist2<X> >.distance(x, y);