Функция 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
являются шаблонами классов, поэтому мой шаблон функции должен принимать параметр шаблона шаблона. Более того, набор и векторные шаблоны имеют различное количество параметров типа, которые необходимо правильно настроить.
Каков наилучший способ обобщения двух шаблонов функций выше в функцию, которая работает с любым совместимым типом контейнера?
Ответы
Ответ 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
требуется заменить больше типов. Существует не только замена T
→ U
, но и замена 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
-
Контейнер с двумя параметрами, отличными от значения по умолчанию: тип параметра (тип значения) и параметр (размер) не-типа:
Реализация
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 за то, что я указал на свои ошибки/упущенные возможности для улучшения.