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

Как `std:: bind()` стандартный алгоритм библиотеки?

Краткая версия моего вопроса такова: как я могу использовать что-то вроде std::bind() со стандартным алгоритмом библиотеки?

Поскольку короткая версия немного лишена деталей, вот немного объяснения: предположим, что у меня есть алгоритмы std::transform(), и теперь я хочу реализовать std::copy() (да, я понимаю, что есть std::copy() в стандартной библиотеке С++). Поскольку я ужасно ленив, я явно хочу использовать существующую реализацию std::transform(). Я мог бы, конечно, сделать это:

struct identity {
    template <typename T>
    auto operator()(T&& value) const -> T&& { return std::forward<T>(value); }
};  
template <typename InIt, typename OutIt>
auto copy(InIt begin, InIt end, OutIt to) -> OutIt {
    return std::transform(begin, end, to, identity());
}

Как-то эта реализация несколько похожа на конфигурацию алгоритма. Например, кажется, что std::bind() должен уметь выполнять задание, но просто используя std::bind() не работает:

namespace P = std::placeholders;
auto copy = std::bind(std::transform, P::_1, P::_2, P::_3, identity());

Проблема заключается в том, что компилятор не может определить соответствующие аргументы шаблона только из алгоритма, и не имеет значения, существует ли & или нет. Есть ли что-то, что может сделать такой подход, как использование std::bind() работы? Поскольку это с нетерпением ждет, я доволен решением, работающим со всем, что уже предлагается для включения в стандарт С++. Кроме того, чтобы уйти с моей леностью, я счастлив сделать некоторые работы перед тем, чтобы более легко использовать их. Подумайте об этом так: в моей роли библиотекаря я собираюсь собрать вещи один раз, чтобы каждый пользователь библиотеки мог лениться: я занят реализацией, но ленивый пользователь.

Если вы хотите иметь готовое тестовое постели: вот полная программа.

#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <utility>
#include <vector>

using namespace std::placeholders;

struct identity {
    template <typename T>
    T&& operator()(T&& value) const { return std::forward<T>(value); }
};


int main()
{
    std::vector<int> source{ 0, 1, 2, 3, 4, 5, 6 };
    std::vector<int> target;

#ifdef WORKS
    std::transform(source.begin(), source.end(), std::back_inserter(target),
                   identity());
#else
    // the next line doesn't work and needs to be replaced by some magic
    auto copy = std::bind(&std::transform, _1, _2, _3, identity());
    copy(source.begin(), source.end(), std::back_inserter(target));
#endif
    std::copy(target.begin(), target.end(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << "\n";
}
4b9b3361

Ответ 1

При попытке std::bind() перегруженной функции компилятор не может определить, какую перегрузку использовать: во время вычисления выражения bind() аргументы функции неизвестны, т.е. разрешение перегрузки не может решить, какая перегрузка подобрать. В С++ [еще?] Нет прямого пути для обработки набора перегрузки как объекта. Шаблоны функций просто генерируют набор перегрузки с одной перегрузкой для каждого возможного экземпляра. То есть вся проблема неспособности std::bind() любого из стандартных алгоритмов библиотеки С++ вращаться вокруг того факта, что стандартные библиотечные алгоритмы являются функциональными шаблонами.

Один из подходов к тому, чтобы иметь такой же эффект, как и std::bind() с использованием алгоритма, - использовать С++ 14 generic lambdas для выполнения привязки, например:

auto copy = [](auto&&... args){
    return std::transform(std::forward<decltype(args)>(args)..., identity());
};

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

namespace nstd {
    auto const transform = [](auto&&... args){
        return std::transform(std::forward<decltype(args)>(args...));
    };
}

Теперь, с подходом к реализации transform(), на самом деле тривиально использовать std::bind() для построения copy():

auto copy = std::bind(nstd::transform, P::_1, P::_2, P::_3, identity());

Несмотря на внешний вид и использование общих лямбда, стоит отметить, что на самом деле требуется примерно то же самое усилие для создания соответствующих объектов функций с использованием только функций, доступных для С++ 11:

struct transform_t {
    template <typename... Args>
    auto operator()(Args&&... args) const
        -> decltype(std::transform(std::forward<decltype(args)>(args)...)) {
        return std::transform(std::forward<decltype(args)>(args)...);
    }
};
constexpr transform_t transform{};

Да, это больше типизация, но это всего лишь разумный небольшой постоянный фактор при использовании общих лямбда, т.е. если объекты, использующие общие lambdas, также имеют версию С++ 11.

Конечно, как только у нас есть функциональные объекты для алгоритмов, он может быть аккуратным, даже не имея при этом std::bind() их, поскольку нам нужно будет упомянуть все не связанные аргументы. В примере это currying (ну, я думаю, что currying применим только к привязке первого аргумента, но является ли он первым или последним аргументом немного случайным), Что делать, если мы имели curry_first() и curry_last() для того, чтобы выполнить первый или последний аргумент? Реализация curry_last() тоже тривиальна (для краткости я использую общую лямбда, но тот же самый переписывающий, что и выше, можно использовать, чтобы сделать его доступным с С++ 11):

template <typename Fun, typename Bound>
auto curry_last(Fun&& fun, Bound&& bound) {
    return [fun = std::forward<Fun>(fun),
            bound = std::forward<Bound>(bound)](auto&&... args){
        return fun(std::forward<decltype(args)>(args)..., bound);
    };
}

Теперь, считая, что curry_last() живет в одном и том же пространстве имен, либо nstd::transform, либо identity() определение copy() может стать:

auto const copy = curry_last(nstd::transform, identity());

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