prestokeys
prestokeys

Reputation: 4849

Generalization of std::transform

Consider this simple generalization of std::transform I wrote for N input iterators:

#include <iostream>
#include <vector>
#include <string>

template <typename InputIterator, typename OutputIterator, typename NaryOperator, typename... InputIterators>
OutputIterator transform (InputIterator first, InputIterator last, OutputIterator result,
NaryOperator op, InputIterators... iterators) {
    while (first != last) {
        *result = op(*first, *iterators++...);
        ++result;  ++first;
    }
    return result;
}

int main() {
    const std::vector<int> a = {1,2,3,4,5};
    const std::vector<double> b = {1.2, 4.5, 0.6, 2.8, 3.1};
    const std::vector<std::string> c = {"hi", "howdy", "hello", "bye", "farewell"};
    std::vector<double> result(5);
    transform (a.begin(), a.end(), result.begin(),
        [](int i, double d, const std::string& s)->double {return i + d + s.length();},
        b.begin(), c.begin());
    for (double x : result) std::cout << x << ' ';  // 4.2 11.5 8.6 9.8 16.1
}

What I want to do now, is allow the vectors a, b, c to have different lengths (and the argument InputIterator last can be removed), in which case transform will continue transforming until the longest vector is used up, using the default values for the vectors that are shorter.

I thought this would just be a matter of resizing all the short containers within the transform function, but the arguments of transform give no information about how long all the containers are. Is there a way to compute within transform how long each of the containers are, thereby getting the maximum length and thus filling the default values for the shorter containers? Ideally using just the syntax:

transform (OutputIterator result, NaryOperator op, InputIterators... iterators);

Update: Following Ramana's idea, I am thinking of using something like:

template <typename OutputIterator, typename NaryOperator, typename... InputIterators>
OutputIterator transform (OutputIterator result, NaryOperator op, InputIterators... first, InputIterators... last) {
    while (true) {
        *result = op((first == last ?
            typename std::iterator_traits<InputIterators>::value_type() : *first++)...);
        ++result;
    }
    return result;
}

But

transform (result.begin(),
    [](int i, double d, const std::string& s)->double {return i + d + s.length();},
    a.begin(), b.begin(), c.begin(), a.end(), b.end(), c.end());

does not compile. I think because the compiler does not know where last... begins.

So I tried this next:

template <typename OutputIterator, typename NaryOperator, typename... InputIteratorsPairs>
OutputIterator transform (OutputIterator result, NaryOperator op, InputIteratorsPairs... pairs) {
    while (true) {
        *result = op((pairs.first == pairs.second ?
            typename InputIteratorsPairs::first_type() : *pairs.first++)...);
        ++result;
    }
    return result;
}

But

transform_ (result.begin(),
    [](int i, double d, const std::string& s)->double {return i + d + s.length();},
    std::make_pair(a.begin(), a.end()), std::make_pair(b.begin(), b.end()), std::make_pair(c.begin(), c.end()));

does not compile either (and I don't like the syntax anyway).

Upvotes: 4

Views: 523

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275310

This is a range based solution. Instead of operating on iterators, we operate on ranges of iterators.

A range is a pair of iterators with some helpers. This is a minimal implementation with only some helpers written:

template<class It> using it_value_type =
  typename std::iterator_traits<It>::value_type;
template<class It> using it_reference =
  typename std::iterator_traits<It>::reference;

template<class It>
struct range_t {
  It b, e;
  range_t():b(),e(){}
  range_t(It s, It f):b(s),e(f){}
  template<class C, class=std::enable_if_t<!std::is_same<std::decay_t<C>,range_t>{}>>
  range_t(C&& c):
    range_t(std::begin(c),std::end(c))
  {}
  It begin() const { return b; }
  It end() const { return e; }

  bool empty() const { return begin()==end(); }
  it_reference<It> front() const { return *begin(); }
  it_reference<It> back() const { return *std::prev(end()); }

  range_t pop_front() const {
    if (empty()) return {};
    else return {std::next(begin()), end()};
  }
};

A function to make creating range_ts easier:

template<class C, class It=std::decay_t<decltype(std::begin(std::declval<C&>()))>>
range_t<It> range( C&& c ) {
  return {std::begin(c), std::end(c)};
}

A helper to make checking a bunch of bools to see if they are all true easier:

bool all_of( std::initializer_list<bool> il ) {
  return std::all_of( il.begin(), il.end(), [](bool b){return b;} );
}

Now for the work. The range-based implementation of transform first:

template<class Sink, class Operator, class...Its>
Sink transform( Sink sink, Operator op, range_t<Its>... srcs ) {
  while(!all_of({srcs.empty()...})) {
    *sink++ = op( (srcs.empty()?it_value_type<Its>{}:srcs.front())... );
    using discard=int[];
    (void)discard{0,
      ((srcs = srcs.pop_front()),0)...
    };
  }
  return sink;
}

And the non-range-based implementation, which just forwards to the above:

template<class Sink, class Operator, class...Srcs>
Sink transform( Sink sink, Operator op, Srcs&&... srcs ) {
  return transform( sink, op, range(srcs)... );
}

they should be able to exist as overloads of each other.

Switching this to stop when the first range ends is easy -- swap all_of for any_of.

live example.

Upvotes: 1

Arne Vogel
Arne Vogel

Reputation: 6666

pairs.first == pairs.second ?
        typename InputIteratorsPairs::first_type() : *pairs.first++

You are value-initializing an iterator on the left side of : instead of the type the iterator points to. Also you have an infinite loop and undefined behavior because you keep incrementing result. Here's a version that fixes these issues (requires <algorithm> and is not necessarily most efficient:

bool any(std::initializer_list<bool> vs)
{
    return std::any_of(begin(vs), end(vs), [](bool b) { return b; });
}

template<typename OutputIterator, typename NaryOperator, typename... InputIteratorsPairs>
OutputIterator transform(OutputIterator result, NaryOperator op, InputIteratorsPairs... pairs) {
    while (any({(pairs.first != pairs.second)...})) {
        *result = op((pairs.first == pairs.second ?
            typename InputIteratorsPairs::first_type::value_type() : *pairs.first++)...);
        ++result;
    }
    return result;
}

Upvotes: 2

Piotr Skotnicki
Piotr Skotnicki

Reputation: 48447

#include <cstddef>
#include <utility>
#include <tuple>
#include <iterator>

bool all(bool a)
{
    return a;
}

template <typename... B>
bool all(bool a, B... b)
{
    return a && all(b...);
}

template <typename OutputIterator, typename NaryOperator, typename... InputIterators, std::size_t... Is>
OutputIterator transform(OutputIterator result, NaryOperator op, std::index_sequence<Is...>, InputIterators... iterators)
{
    auto tuple = std::make_tuple(iterators...);
    while (!all(std::get<2*Is>(tuple) == std::get<2*Is + 1>(tuple)...))
    {
        *result = op((std::get<2*Is>(tuple) != std::get<2*Is + 1>(tuple)
              ? *std::get<2*Is>(tuple)++
              : typename std::iterator_traits<typename std::tuple_element<2*Is, decltype(tuple)>::type>::value_type{})...);
        ++result;
    }
    return result;
}

template <typename OutputIterator, typename NaryOperator, typename... InputIterators>
OutputIterator transform(OutputIterator result, NaryOperator op, InputIterators... iterators)
{
    return transform(result, op, std::make_index_sequence<sizeof...(InputIterators)/2>{}, iterators...);
}

Tests:

int main()
{
    const std::vector<int> a = {1,2,3,4,5};
    const std::vector<double> b = {1.2, 4.5, 0.6, 2.8, 3.1};
    const std::vector<std::string> c = {"hi", "howdy", "hello", "bye", "farewell"};
    std::vector<double> result(5);

    transform(result.begin(),
        [] (int i, double d, const std::string& s) -> double
        {
            return i + d + s.length();
        },
        a.begin(), a.end(),
        b.begin(), b.end(),
        c.begin(), c.end());

    for (double x : result) std::cout << x << ' ';
}

Output:

4.2 11.5 8.6 9.8 16.1 

DEMO

Upvotes: 4

Related Questions