Алгоритмы C ++, которые создают свое хранилище вывода вместо применения к существующему хранилищу? - PullRequest
1 голос
/ 26 августа 2011

Алгоритмы C ++ std определяют ряд алгоритмов, которые принимают входную и выходную последовательности, и создают элементы выходной последовательности из элементов входной последовательности. (Лучший пример: std::transform.)

Очевидно, что алгоритмы std требуют iterators, поэтому нет сомнений в том, что контейнер для OutputIterator должен существовать до вызова алгоритма.

То есть:

std::vector<int> v1; // e.g. v1 := {1, 2, 3, 4, 5};

std::vector<int> squared;
squared.reserve(v1.size()); // not strictly necessary
std::transform(v1.begin(), v1.end(), std::back_inserter(squared), 
               [](int x) { return x*x; } ); // λ for convenience, needn't be C++11

И это хорошо, если использовать стандартную библиотеку . Когда я нахожу итераторы слишком громоздкими, я часто обращаюсь к Boost.Range , чтобы упростить ситуацию.

В этом случае, однако, похоже, что алгоритмы мутации в Boost.Range также используют OutputIterators.

Так что я сейчас задаюсь вопросом, есть ли какая-нибудь удобная библиотека , которая позволяет мне писать:

std::vector<int> const squared = convenient::transform(v1, [](int x) { return x*x; });

- а если его нет, есть ли причина, по которой его нет ?

Редактировать: пример реализации (не уверен, будет ли это работать во всех случаях и является ли это наиболее идеальным):

template<typename C, typename F>
C transform(C const& input, F fun) {
   C result;
   std::transform(input.begin(), input.end(), std::back_inserter(result), fun);
   return result;
}       

(Примечание: я думаю, что convenient::transform будет иметь те же характеристики производительности, что и рукописные, поскольку возвращаемый вектор не будет скопирован из-за (N) RVO. В любом случае, я думаю, что производительность для этого вопроса вторична.)


Редактировать / Примечание: Из ответов (на самом деле) комментариев, приведенных до сих пор, Дэвид дает очень хороший базовый общий пример.

И Люк упоминает о возможной проблеме с std::back_inserter относительно. типичность.

И то, и другое просто показывает, почему я сам не решаюсь заняться этим и почему «правильная» (должным образом протестированная) библиотека предпочтительнее, чем кодирование самого себя.

Мой вопрос, выделенный жирным шрифтом выше, а именно: есть один, или есть причина, по которой его нет остается в основном без ответа.

Ответы [ 5 ]

3 голосов
/ 26 августа 2011

Это не означает, что ответ на сам вопрос, это дополнение к другим ответам - но это не вписывается в комментарии.

хорошо - что, если вы хотели списокили deque, или какой-то другой контейнер типа последовательности - это довольно ограничивающее.

namespace detail {

template<typename Iter, typename Functor>
struct transform {
    Iter first, last;
    Functor functor;

    template<typename Container> // SFINAE is also available here
    operator Container()
    {
        Container c;
        std::transform(first, last, std::back_inserter(c), std::forward<Functor>(functor));
        return c;
    }
};

} // detail

template<typename Iter, typename Functor>
detail::transform<Iter, typename std::decay<Functor>::type>
transform(Iter first, Iter last, Functor&& functor)
{ return { first, last, std::forward<Functor>(functor) }; }

Хотя это будет работать с несколькими контейнерами, оно все же не очень универсально, поскольку требует, чтобы контейнер был «совместимым»с std::back_inserter(c) (BackInsertable?).Возможно, вы могли бы использовать SFINAE, чтобы вместо этого использовать std::inserter с c.begin(), если c.push_back() недоступно (оставлено в качестве упражнения для читателя).

Все это также предполагает, что контейнер DefaultConstructible -рассмотрим контейнеры, которые используют выделенные области.Предположительно , что потеря универсальности - это особенность, поскольку мы только пытаемся охватить «простейшие» варианты использования.

И это на самом деле, хотя я бы не использовал такую ​​библиотеку: Iне против создать контейнер рядом с алгоритмом, чтобы отделить задачи .(Полагаю, это можно считать моим ответом на вопрос.)

2 голосов
/ 26 августа 2011

ИМХО, смысл такого алгоритма в том, чтобы быть универсальным , т.е. главным образом независимым от контейнера.Вы предлагаете, чтобы функция transform была очень специфичной и возвращала std::vector, а что если вы хотели бы list или deque или какой-либо другой контейнер типа последовательности - это довольно ограничивающее.1008 * Почему бы не завернуть, если вас это так раздражает?Создайте свой собственный маленький заголовок утилит, который делает это - в конце концов, это довольно тривиально ...

1 голос
/ 09 февраля 2012

Boost.Range.Adaptors можно рассматривать как алгоритмы возврата контейнеров. Почему бы не использовать их?

Единственное, что нужно сделать, - это определить новый адаптер диапазона create<T>, который можно подключить к адаптированным диапазонам и получить нужный контейнер результатов:

template<class T> struct converted{}; // dummy tag class

template<class FwdRange, class T>
T operator|(FwdRange const& r, converted<T>){
  return T(r.begin(), r.end());
}

Да, вот и все. Больше ничего не нужно. Просто укажите это в конце списка адаптеров.

Здесь может быть живым примером на Ideone. Увы, это не так, потому что Ideone не обеспечивает Boost в режиме C ++ 0x ... ме. В любом случае, вот main и вывод:

int main(){
  using namespace boost::adaptors;
  auto range = boost::irange(1, 10);
  std::vector<int> v1(range.begin(), range.end());

  auto squared = v1 | transformed([](int i){ return i * i; });
  boost::for_each(squared, [](int i){ std::cout << i << " "; });
  std::cout << "\n========================\n";
  auto modded = squared | reversed
                        | filtered([](int i){ return (i % 2) == 0; })
                        | converted<std::vector<int>>(); // gimme back my vec!
  modded.push_back(1);
  boost::for_each(modded, [](int i){ std::cout << i << " "; });
}

Выход:

1 4 9 16 25 36 49 64 81
========================
64 36 16 4 1
1 голос
/ 26 августа 2011

Опять же, нет ответа, а скорее продолжение комментариев к другому ответ

Об универсальности возвращаемого типа в коде вопросов

Код в его нынешнем виде не позволяет преобразовать тип возвращаемого значения, но это можно легко решить, предоставив два шаблона:

template <typename R, typename C, typename F>
R transform( C const & c, F f ) {_
    R res;
    std::transform( c.begin(), c.end(), std::back_inserter(res), f );
    return res;
}
template <typename C, typename F>
C transform( C const & c, F f ) {
    return transform<C,C,F>(c,f);
}
std::vector<int> src;
std::vector<int> v = transform( src, functor );
std::deque<int>  d = transform<std::deque<int> >( src, functor );
1 голос
/ 26 августа 2011

Нет единого и правильного способа включения

std::vector<int> const squared = 
             convenient::transform(v1, [](int x) { return x*x; });

без потенциальной потери производительности.Вам либо нужен явный

std::vector<int> const squared = 
             convenient::transform<std::vector> (v1, [](int x) { return x*x; });

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

Разрешение алгоритму брать контейнер вместо итераторов также не является решением.Таким образом, алгоритм не может знать, как правильно получить первый и последний элемент.Например, массив long int не имеет методов для begin(), end() и length(), не во всех контейнерах есть итераторы произвольного доступа, не определены operator[].Таким образом, нет действительно общего способа брать контейнеры.

Еще одна возможность, которая допускает независимые от контейнера алгоритмы возврата контейнеров, - это некая универсальная фабрика (см. В прямом эфире на http://ideone.com/7d4E2):

// (not production code; is even lacking allocator-types)
//-- Generic factory. -------------------------------------------
#include <list>
template <typename ElemT, typename CacheT=std::list<ElemT> >
struct ContCreator {

    CacheT cache; // <-- Temporary storage.

    // Conversion to target container type.
    template <typename ContT>
    operator ContT () const {
        // can't even move ...
        return ContT (cache.begin(), cache.end());
    }
};

Не так много магии, кромешаблонный оператор приведения. Затем вы возвращаете эту вещь из своего алгоритма:

//-- A generic algorithm, like std::transform :) ----------------
ContCreator<int> some_ints () {
    ContCreator<int> cc;
    for (int i=0; i<16; ++i) {
        cc.cache.push_back (i*4);
    }
    return cc;
}

и, наконец, используете это так, чтобы написать магический код:

//-- Example. ---------------------------------------------------
#include <vector>
#include <iostream>
int main () {
    typedef std::vector<int>::iterator Iter;
    std::vector<int> vec = some_ints();
    for (Iter it=vec.begin(), end=vec.end(); it!=end; ++it) {
        std::cout << *it << '\n';
    }
}

Как видите, в operator Tесть копия диапазона.

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

Редактировать: Как указывает Дэвид, вы можетеКонечно, выполняйте реальную работу внутри оператора преобразования, что, вероятно, не потребует дополнительных затрат (если немного больше работы, то это можно сделать более удобно; это только для демонстрации):

#include <list>
template <typename ElemT, typename Iterator>
struct Impl {
    Impl(Iterator it, Iterator end) : it(it), end(end) {}

    Iterator it, end;

    // "Conversion" + Work.
    template <typename ContT>
    operator ContT () {
        ContT ret;
        for ( ; it != end; ++it) {
            ret.push_back (*it * 4);
        }
        return ret;    
    }
};

template <typename Iterator>
Impl<int,Iterator> foo (Iterator begin, Iterator end) {
    return Impl<int,Iterator>(begin, end);
}

#include <vector>
#include <iostream>
int main () {
    typedef std::vector<int>::iterator Iter;

    const int ints [] = {1,2,4,8};
    std::vector<int> vec = foo (ints, ints + sizeof(ints) / sizeof(int));

    for (Iter it=vec.begin(), end=vec.end(); it!=end; ++it) {
        std::cout << *it << '\n';
    }
}

Одно требованиечто у цели есть метод push_back. Использование std::distance для резервирования размера может привести к неоптимальной производительности, если целевой итератор-контейнер не является случайным доступом.

...