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

Функция std:: transform, которая возвращает преобразованный контейнер

Я пытаюсь реализовать функцию, аналогичную алгоритму std::transform, но вместо того, чтобы принимать выходной итератор аргументом, я хочу создать и вернуть контейнер с преобразованными элементами ввода.

Скажем, что он называется transform_container и принимает два аргумента: container и functor. Он должен возвращать один и тот же тип контейнера, но, возможно, параметризован другим типом элемента (Functor может возвращать элемент другого типа).

Я хотел бы использовать свою функцию, как в примере ниже:

std::vector<int> vi{ 1, 2, 3, 4, 5 };
auto vs = transform_container(vi, [] (int i) { return std::to_string(i); }); 
//vs will be std::vector<std::string>
assert(vs == std::vector<std::string>({"1", "2", "3", "4", "5"}));

std::set<int> si{ 5, 10, 15 };
auto sd = transform_container(si, [] (int i) { return i / 2.; }); 
//sd will be of type std::set<double>
assert(sd == std::set<double>({5/2., 10/2., 15/2.}));

Мне удалось написать две функции - одну для std::set и одну для std::vector - которые, похоже, работают правильно. Они идентичны, за исключением имени контейнера. Их код указан ниже.

template<typename T, typename Functor>
auto transform_container(const std::vector<T> &v, Functor &&f) -> std::vector<decltype(f(*v.begin()))>
{
    std::vector<decltype(f(*v.begin()))> ret;
    std::transform(std::begin(v), std::end(v), std::inserter(ret, ret.end()), f);
    return ret;
}

template<typename T, typename Functor>
auto transform_container(const std::set<T> &v, Functor &&f) -> std::set<decltype(f(*v.begin()))>
{
    std::set<decltype(f(*v.begin()))> ret;
    std::transform(std::begin(v), std::end(v), std::inserter(ret, ret.end()), f);
    return ret;
}

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

Каков наилучший способ обобщения двух шаблонов функций выше в функцию, которая работает с любым совместимым типом контейнера?

4b9b3361

Ответ 1

Простейшие случаи: сопоставление типов контейнеров

Для простого случая, когда тип ввода совпадает с типом вывода (который я реализовал, не то, о чем вы просите), переходите на один уровень выше. Вместо указания типа T, который использует ваш контейнер и который пытается специализироваться на vector<T> и т.д., Просто укажите тип самого контейнера:

template <typename Container, typename Functor>
Container transform_container(const Container& c, Functor &&f)
{
    Container ret;
    std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
    return ret;
}

Дополнительная сложность: совместимые типы значений

Поскольку вы хотите попытаться изменить тип элемента, хранящийся в контейнере, вам нужно будет использовать параметр шаблона шаблона и изменить T, который использует возвращенный контейнер.

template <
    template <typename T, typename... Ts> class Container,
    typename Functor,
    typename T, // <-- This is the one we'll override in the return container
    typename U = std::result_of<Functor(T)>::type,
    typename... Ts
>
Container<U, Ts...> transform_container(const Container<T, Ts...>& c, Functor &&f)
{
    Container<U, Ts...> ret;
    std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
    return ret;
}

Что из несовместимых типов значений?

Это только нас ждет. Он отлично работает с преобразованием от signed до unsigned, но когда он разрешен с помощью T=int и S=std::string, и обрабатывает множества, он пытается создать экземпляр std::set<std::string, std::less<int>, ...> и, следовательно, не компилируется.

Чтобы исправить это, мы хотим взять произвольный набор параметров и заменить экземпляры T на U, даже если они являются параметрами для других параметров шаблона. Таким образом, std::set<int, std::less<int>> должно стать std::set<std::string, std::less<std::string>> и т.д. Это связано с некоторыми мета-программированием шаблонов, как это было предложено другими ответами.

Шаблоны метапрограммирования для спасения

Создайте шаблон, назовите его replace_type и переведите T в U и K<T> в K<U>. Сначала давайте рассмотрим общий случай. Если это не шаблонный тип и не соответствует T, его тип должен оставаться K:

template <typename K, typename ...>
struct replace_type { using type = K; };

Тогда специализация. Если это не шаблонный тип и он соответствует T, его тип должен стать U:

template <typename T, typename U>
struct replace_type<T, T, U> { using type = U; };

И, наконец, рекурсивный шаг для обработки параметров шаблонов. Для каждого типа в параметрах шаблонного типа замените типы соответственно:

template <template <typename... Ks> class K, typename T, typename U, typename... Ks>
struct replace_type<K<Ks...>, T, U> 
{
    using type = K<typename replace_type<Ks, T, U>::type ...>;
};

И, наконец, обновите transform_container, чтобы использовать replace_type:

template <
    template <typename T, typename... Ts> class Container,
    typename Functor,
    typename T,
    typename U = typename std::result_of<Functor(T)>::type,
    typename... Ts,
    typename Result = typename replace_type<Container<T, Ts...>, T, U>::type
>
Result transform_container(const Container<T, Ts...>& c, Functor &&f)
{
    Result ret;
    std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
    return ret;
}

Является ли это полным?

Проблема с этим подходом заключается в том, что это не обязательно безопасно. Если вы переходите с Container<MyCustomType> в Container<SomethingElse>, это, вероятно, отлично. Но при преобразовании из Container<builtin_type> в Container<SomethingElse> правдоподобно, что другой параметр шаблона не должен быть преобразован из builtin_type в SomethingElse. Кроме того, альтернативные контейнеры, такие как std::map или std::array, приносят больше проблем стороне.

Обработка std::map и std::unordered_map не так уж плоха. Основная проблема заключается в том, что replace_type требуется заменить больше типов. Существует не только замена TU, но и замена std::pair<T, T2>std::pair<U, U2>. Это увеличивает уровень озабоченности нежелательными заменами типа, поскольку в полете больше одного типа. Тем не менее, вот что я нашел для работы; обратите внимание, что при тестировании мне нужно было указать тип возврата лямбда-функции, которая преобразует мои пары карт:

// map-like classes are harder. You have to replace both the key and the key-value pair types
// Give a base case replacing a pair type to resolve ambiguities introduced below
template <typename T1, typename T2, typename U1, typename U2>
struct replace_type<std::pair<T1, T2>, std::pair<T1, T2>, std::pair<U1, U2>>
{
    using type = std::pair<U1, U2>;
};

// Now the extended case that replaces T1->U1 and pair<T1,T2> -> pair<T2,U2>
template <template <typename...> class K, typename T1, typename T2, typename U1, typename U2, typename... Ks>
struct replace_type<K<T1, T2, Ks...>, std::pair<const T1, T2>, std::pair<const U1, U2>>
{
    using type = K<U1, U2, 
        typename replace_type< 
            typename replace_type<Ks, T1, U1>::type,
            std::pair<const T1, T2>,
            std::pair<const U1, U2>
        >::type ...
    >;
};

Как насчет std:: array?

Обработка std::array добавляет боль, поскольку его параметры шаблона не могут быть выведены в приведенном выше шаблоне. Как отмечает Jarod42, это связано с его параметрами, включая значения, а не только типы. Я получил отчасти добавление специализаций и введение вспомогательного contained_type, который извлекает T для меня (боковая заметка, для каждого конструктора это лучше написано как гораздо более простая typename Container::value_type и работает для всех типов, которые я обсуждал здесь), Даже без спецификаций std::array это позволяет мне упростить мой шаблон transform_container следующим образом (это может быть выигрыш даже без поддержки std::array):

template <typename T, size_t N, typename U>
struct replace_type<std::array<T, N>, T, U> { using type = std::array<U, N>; };

// contained_type<C>::type is T when C is vector<T, ...>, set<T, ...>, or std::array<T, N>.
// This is better written as typename C::value_type, but may be necessary for bad containers
template <typename T, typename...>
struct contained_type { };

template <template <typename ... Cs> class C, typename T, typename... Ts>
struct contained_type<C<T, Ts...>> { using type = T; };

template <typename T, size_t N>
struct contained_type<std::array<T, N>> { using type = T; };

template <
    typename Container,
    typename Functor,
    typename T = typename contained_type<Container>::type,
    typename U = typename std::result_of<Functor(T)>::type,
    typename Result = typename replace_type<Container, T, U>::type
>
Result transform_container(const Container& c, Functor &&f)
{
    // as above
}

Однако в текущей реализации transform_container используется std::inserter, которая не работает с std::array. Хотя можно сделать больше специализаций, я собираюсь оставить это как упражнение для суп-шаблона для заинтересованного читателя. В большинстве случаев я бы предпочел жить без поддержки std::array.

Просмотр кумулятивного живого примера


Полное раскрытие информации: в то время как на этот подход повлиял Али, цитирующий ответ Керрека С.Б., мне не удалось получить это для работы в Visual Studio 2013, поэтому я сам построил эту альтернативу. Большое спасибо частям оригинального ответа Kerrek SB по-прежнему необходимы, а также подталкивание и поощрение от Constructor и Jarod42.

Ответ 2

Некоторые замечания

Следующий метод позволяет преобразовывать контейнеры любого типа из стандартной библиотеки (существует проблема с std::array, см. ниже). Единственное требование для контейнера состоит в том, что он должен использовать классы по умолчанию std::allocator, std::less, std::equal_to и std::hash объекты функций. Итак, у нас есть 3 группы контейнеров из стандартной библиотеки:

  • Контейнеры с одним параметром типа нестандартного типа (тип значения):

    • std::vector, std::deque, std::list, std::forward_list, [std::valarray]
    • std::queue, std::priority_queue, std::stack
    • std::set, std::unordered_set
  • Контейнеры с двумя параметрами типа шаблона не по умолчанию (тип ключа и тип значения):

    • std::map, std::multi_map, std::unordered_map, std::unordered_multimap
  • Контейнер с двумя параметрами, отличными от значения по умолчанию: тип параметра (тип значения) и параметр (размер) не-типа:

    • std::array

Реализация

convert_container Вспомогательный класс преобразует типы известного типа входного контейнера (InputContainer) и тип выходного значения (OutputType) в тип выходного контейнера (typename convert_container<InputContainer, Output>::type):

template <class InputContainer, class OutputType>
struct convert_container;

// conversion for the first group of standard containers
template <template <class...> class C, class IT, class OT>
struct convert_container<C<IT>, OT>
{
    using type = C<OT>;
};

// conversion for the second group of standard containers
template <template <class...> class C, class IK, class IT, class OK, class OT>
struct convert_container<C<IK, IT>, std::pair<OK, OT>>
{
    using type = C<OK, OT>;
};

// conversion for the third group of standard containers
template
    <
        template <class, std::size_t> class C, std::size_t N, class IT, class OT
    >
struct convert_container<C<IT, N>, OT>
{
    using type = C<OT, N>;
};

template <typename C, typename T>
using convert_container_t = typename convert_container<C, T>::type;

transform_container реализация функции:

template
    <
        class InputContainer,
        class Functor,
        class InputType = typename InputContainer::value_type,
        class OutputType = typename std::result_of<Functor(InputType)>::type,
        class OutputContainer = convert_container_t<InputContainer, OutputType>
    >
OutputContainer transform_container(const InputContainer& ic, Functor f)
{
    OutputContainer oc;

    std::transform(std::begin(ic), std::end(ic), std::inserter(oc, oc.end()), f);

    return oc;
}

Пример использования

Смотрите живой пример со следующими преобразованиями:

  • std::vector<int> -> std::vector<std::string>,
  • std::set<int> -> std::set<double>,
  • std::map<int, char> -> std::map<char, int>.

Проблемы

std::array<int, 3> -> std::array<double, 3> преобразование не скомпилируется, потому что std::array не имеет метода insert, который необходим из-за std::inserter). Функция transform_container также не должна работать по этой причине со следующими контейнерами: std::forward_list, std::queue, std::priority_queue, std::stack, [std::valarray].

Ответ 3

Выполнение этого в целом будет довольно сложно.

Сначала рассмотрим std::vector<T, Allocator=std::allocator<T>>, и пусть ваш функтор преобразует T->U. Мало того, что мы должны сопоставить аргумент первого типа, но на самом деле мы должны использовать Allocator<T>::rebind<U> для получения второго. Это означает, что нам нужно знать, что второй аргумент - это распределитель, в первую очередь... или нам нужны некоторые механизмы, чтобы проверить, что он имеет шаблон члена rebind и использует его.

Затем рассмотрим std::array<T, N>. Здесь нам нужно знать, что второй аргумент должен быть скопирован буквально на наш std::array<U, N>. Возможно, мы можем принимать параметры без типа без изменений, параметры типа повторной привязки, которые имеют шаблон элемента повторной привязки, и заменить литерал T на U?

Теперь std::map<Key, T, Compare=std::less<Key>, Allocator=std::allocator<std::pair<Key,T>>>. Мы должны взять Key без изменений, заменить T на U, взять Compare без изменений и перестроить Allocator на std::allocator<std::pair<Key, U>>. Это немного сложнее.

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

Ответ 4

Основной трудностью является получение типа контейнера Container из Conainer<T>. Я бесстыдно украл код из шаблона метапрограммирования: (trait for?), Рассекающего заданный шаблон в типы T < T2, T3 N, T4,... > , в частности, Ответ Kerrek SB (принятый ответ), поскольку я не знаком с метапрограммированием шаблонов.

#include <algorithm>
#include <cassert>
#include <type_traits>

// stolen from Kerrek SB answer
template <typename T, typename ...>
struct tmpl_rebind {
    typedef T type;
};

template <template <typename ...> class Tmpl, typename ...T, typename ...Args>
struct tmpl_rebind<Tmpl<T...>, Args...> {
    typedef Tmpl<Args...> type;
};
// end of stolen code

template <typename Container,
          typename Func,
          typename TargetType = typename std::result_of<Func(typename Container::value_type)>::type,
          typename NewContainer = typename tmpl_rebind<Container, TargetType>::type >
NewContainer convert(const Container& c, Func f) {

    NewContainer nc;

    std::transform(std::begin(c), std::end(c), std::inserter(nc, std::end(nc)), f);

    return nc;
}

int main() {

    std::vector<int> vi{ 1, 2, 3, 4, 5 };
    auto vs = convert(vi, [] (int i) { return std::to_string(i); });
    assert( vs == std::vector<std::string>( {"1", "2", "3", "4", "5"} ) );

    return 0;
}

Я тестировал этот код с gcc 4.7.2 и clang 3.5 и работал как ожидалось.

Как указывает Якк, существует целый ряд предостережений с этим кодом: "... если ваша перегруппировка заменит все аргументы или только первую? Неопределенно. Если он рекурсивно заменит T0 на T1 в более поздних аргументах? То есть std::map<T0, std::less<T0>>std::map<T1, std::less<T1>>?" Я также вижу ловушки с указанным выше кодом (например, как обращаться с разными распределителями, см. Также Бесполезный ответ).

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


Большое спасибо Constructor, dyp и Yakk за то, что я указал на свои ошибки/упущенные возможности для улучшения.