3

Let's say I have a a vector<vector<int>>. I want to use ranges::transform in such a way that I get

vector<vector<int>> original_vectors;
using T = decltype(ranges::views::transform(original_vectors[0], [&](int x){
              return x;
          }));
vector<int> transformation_coeff;
vector<T> transformed_vectors;
for(int i=0;i<n;i++){
    transformed_vectors.push_back(ranges::views::transform(original_vectors[i], [&](int x){
        return x * transformation_coeff[i];
    }));
}

Is such a transformation, or something similar, currently possible in C++?

I know its possible to simply store the transformation_coeff, but it's inconvenient to apply it at every step. (This will be repeated multiple times so it needs to be done in O(log n), therefore I can't explicitly apply the transformation).

2 Answers2

6

Yes, you can have a vector of ranges. The problem in your code is that you are using a temporary lambda in your using statement. Because of that, the type of the item you are pushing into the vector later is different from T. You can solve it by assigning the lambda to a variable first:

vector<vector<int>> original_vectors;
auto lambda = [&](int x){return x;};
using T = decltype(ranges::views::transform(original_vectors[0], lambda));
vector<T> transformed_vectors;
transformed_vectors.push_back(ranges::views::transform(original_vectors[0], lambda));
Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
  • Thanks, I suspected that may be the issue. However I would like to use a different int -> int transform on each vector, so how would I do that? – Aditya Jain Nov 05 '22 at 07:20
  • 2
    That would be harder to achieve, as each range would have a different type. You can probably create a tuple of ranges or a range of ranges. Could you provide the different transforms you want to use? – joergbrech Nov 05 '22 at 07:51
  • 1
    @adityajain I doubt that is possible since `transform_view` of different transformations results into different types, so they can't be put into a single vector. One thing to note is that even if it is possible, any transformation are calculated only when the corresponding values are being accessed, so there isn't a real different between storing the transformations in this `vector` and storing the transformations in a separate `vector`. – Ranoiaetep Nov 05 '22 at 07:54
  • @joergbrech I've made it more similar to the final code I need, and shown the transformations as well. How would I use a range of ranges to accomplish this? – Aditya Jain Nov 05 '22 at 15:33
  • 1
    If you need different transformations for each range you could convert each lambda to a `std::function`, so you use the same type everywhere. – IlCapitano Nov 05 '22 at 22:33
1

It is not possible in general to store different ranges in a homogeneous collection like std::vector, because different ranges usually have different types, especially if transforms using lambdas are involved. No two lambdas have the same type and the type of the lambda will be part of the range type. If the signatures of the functions you want to pass to the transform are the same, you could wrap the lambdas in std::function as suggested by @IlCapitano (https://godbolt.org/z/zGETzG4xW). Note that this comes at the cost of the additional overhead std::function entails.

A better option might be to create a range of ranges.

If I understand you correctly, you have a vector of n vectors, e.g.

std::vector<std::vector<int>> original_vector = {
    {1,  5, 10},
    {2,  4,  8},
    {5, 10, 15}
};

and a vector of n coefficients, e.g.

std::vector<int> transformation_coeff = {2, 1, 3};

and you want a range of ranges representing the transformed vectors, where the ith range represents the ith vector's elements which have been multiplied by the ith coefficient:

{
    { 2, 10, 20}, // {1,  5, 10} * 2
    { 2,  4,  8}, // {2,  4,  8} * 1
    {15, 30, 45}  // {5, 10, 15} * 3
}

Did I understand you correctly? If yes, I don't understand what you mean with your complexity requirement of O(log n). What does n refer to in this scenario? How would this calculation be possible in less than n steps? Here is a solution that gives you the range of ranges you want. Evaluating this range requires O(n*m) multiplications, where m is an upper bound for the number of elements in each inner vector. I don't think it can be done in less steps because you have to multiply each element in original_vector once. Of course, you can always just evaluate part of the range, because the evaluation is lazy.

C++20

The strategy is to first create a range for the transformed i-th vector given the index i. Then you can create a range of ints using std::views::iota and transform it to the inner ranges:

auto transformed_ranges = std::views::iota(0) | std::views::transform(
    [=](int i){

        // get a range containing only the ith inner range
        auto ith = original_vector | std::views::drop(i) | std::views::take(1) | std::views::join;

        // transform the ith inner range
        return ith | std::views::transform(
            [=](auto const& x){
                return x * transformation_coeff[i];
            }
        );
    }
);

You can now do

for (auto const& transformed_range : transformed_ranges){
    for (auto const& val : transformed_range){
        std::cout << val << " ";
    }
    std::cout<<"\n";
}

Output:

2 10 20 
2 4 8 
15 30 45 

Full Code on Godbolt Compiler Explorer

C++23

This is the perfect job for C++23's std::views::zip_transform:

auto transformed_ranges = std::views::zip_transform(
    [=](auto const& ith, auto const& coeff){
        return ith | std::views::transform(
            [=](auto const& x){
                return x * coeff;
            }
        );
    },
    original_vector,
    transformation_coeff
);

It's a bit shorter and has the added benefit that transformation_coeff is treated as a range as well:

  • It is more general, because we are not restricted to std::vectors
  • In the C++20 solution you get undefined behaviour without additional size checking if transformation_coeff.size() < original_vector.size() because we are indexing into the vector, while the C++23 solution would just return a range with fewer elements.

Full Code on Godbold Compiler Explorer

joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • 1
    Thanks, this is what I needed. By a low complexity requirement I meant it shouldn't take `O(sum of number of elements)`, since I would only be checking `O(log n)` elements in the array. `O(original_vectors.size())` is fine. From my understanding of `std::ranges` the 2nd (if I use `original_vector` by reference) and 3rd should be fast even if there are 30 vectors in `original_vectors` each with a size of 1 million. – Aditya Jain Nov 07 '22 at 05:30