6

I was assigned a task where I need to solve a problem given several constraints. The point is to enforce the use of STL algorithms, iterators, and new c++20 functionality including things like ranges. However I've been reading on ranges for hours and I still can't figure out how I can implement the problem given all the constraints. I've simplified the problem and removed the specific details to make it more generic.

The Problem:

Write a function that 1) takes in a vector of custom objects input 2) returns a vector of a different type that includes an element for each object in input that satisfies some conditions. The value added is based on the properties of the input objects.

I realize this may sound obscure so here's a simple example. For an input vector of Shapes where each has a name and an area:

vector<Shapes> input{ { "Square", 10 }, { "Triangle", 30 } , { "Square", 1 }, { "Circle", 30 }, { "Triangle", 15 } };

return a vector of enums

enum Color { RED, BLUE, GREEN };

such that an enum is added for each Square or Circle. The value of the enum is determined based on the area of each Shape. So, let's say, if the area is above 20, RED is added, otherwise, GREEN is added.

So in this case we'd return { GREEN, GREEN, RED }

This is all well and good and could be implemented in a myriad of ways, what makes it very difficult are the constraints.

The Constraints:

  • no loops or recursion
  • no std::for_each
  • no data structures other than std::vector
  • no memory allocation (other than a one-time allocation for the return vector)
  • no by-reference lambda captures or mutable lambdas
  • cannot modify the input vector

My professor claims that "c++20 ranges make this task particularly simple." But even after reading on ranges for hours I'm not even sure where would I begin. My current train of thought is to create a std::view and filter it based on the conditions (Squares & Circles) but then I'm not sure how I would create a new vector of a different type and add elements to it based on the properties of the elements in the view without using loops, for_each, or by-reference lambdas..

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
Adham
  • 121
  • 7
  • 2
    "*My current train of thought is to create a std::view and filter it based on the conditions*" Try referring to the [cppreference](https://en.cppreference.com/w/cpp/ranges/filter_view)'s example to create a lambda that needs to pass in `views::filter` and `views::transform`. – 康桓瑋 Oct 06 '22 at 06:36
  • @康桓瑋 I did dig into the documentation quite a bit and, through using `std::views::filter` and creating a lambda function, I currently have a filter_view of the input vector elements that satisfy the conditions. The next part is where I'm stuck.. – Adham Oct 06 '22 at 06:41
  • Try using `views::transform` to convert the filtered element's value into a different enum value based on its area value. – 康桓瑋 Oct 06 '22 at 06:44
  • you would need `rangess::to_vector` (which is implemented with a for loop) to turn your filtered and transformed range into a vector, which c++20 doesn't provide (its only in c++23). You would have to use range-v3 or use a for loop. The for loop always exists somewhere, ranges are nice because they push them down into code you don't have to worry about – Tom Huntington Oct 06 '22 at 06:58
  • @康桓瑋 Wow it actually turned out to be much simpler than I expected. I guess I just didn't expect that `views::transform` could also transform that data type of the view elements. After transforming I simply created a vector using the `begin` & `end` iterators of the resulting view (as @cptFracassa said in his answer) and BAM there it was. Appreciate your support. – Adham Oct 06 '22 at 07:07
  • 1
    The first time ever I read a C++ professor wants students to use the STL instead of writing C code. Congrats to him! – Enlico Oct 06 '22 at 07:11
  • But probably, given the didactic context, maybe he should have suggested using Range-v3 rather than C++20's ranges, as the former provides more stuff. – Enlico Oct 06 '22 at 07:12
  • @Enlico Lol I can attest to that! after years of university-level programming, this is the first time I encounter such a problem :D – Adham Oct 06 '22 at 07:14
  • Hmm this stuff is interesting but imo pushing control code (for / container types.. ) out of sight isn't necessarily something you want. – StarShine Oct 06 '22 at 12:43
  • whoops, I guess you can hide the loop in the vector constructor. Although, this is still imperative procedural code. The main benefit of ranges it to avoid imperative procedural code. See it done [here](https://godbolt.org/z/sdMejWW4e) in one expression with range-v3 – Tom Huntington Oct 06 '22 at 23:58
  • @TomHuntington that's pretty cool, hopefully will be added to the standard soon. Any idea of any advantages this has over explicit construction (other than being cleaner and more concise) in terms of space or time complexity? I'm still not very clear on how ranges operate under the hood – Adham Oct 07 '22 at 01:55
  • `to_vector` should not cause a slowdown, however in general ranges will be slower than c style code, but assembly coders say the same about c. 99% of the time the problem is not speed, but developer productivity. And when you do need speed even standard assembly is not fast enough, you have to use simd or the gpu. For how ranges operate upder the hood see https://www.youtube.com/watch?v=d3qY4dZ2r4w but its not really worth knowing, better just to learn simd or gpu if you want speed. – Tom Huntington Oct 07 '22 at 03:48
  • @Adham: `ranges::to` has been added to C++23 (https://en.cppreference.com/w/cpp/ranges/to) , but there's no compiler support for it yet. – cptFracassa Oct 07 '22 at 06:00

1 Answers1

10

If you simply want to transform every shape to an enum, all you need is:

auto colors = input | transform([](const Shapes& s){return s.size > 20 ? RED : GREEN;});

colors can then be looped through in a for-loop:

for (const auto& c : colors)

Or be made into a new vector:

std::vector<Color> colorEnums{colors.begin(), colors.end()};

...though C++23's ranges::to would be helpful for the latter.

If you want to only generate the color elements for some elements in your input vector (ex.: not triangles), add filter before transform, e.g.:

auto colors = input | 
    filter([](const Shapes& s){return s.type != "Triangle";}) | 
    transform([](const Shapes& s){return s.size > 20 ? RED : GREEN;});

Note that having filter after transform would work too, but be less efficient, as the code would then transform all elements, before discarding some of them. So use filter as early as possible.

Barry
  • 286,269
  • 29
  • 621
  • 977
cptFracassa
  • 893
  • 4
  • 14
  • I'm accepting this because it correctly shows the second (`views::transform`) and the third (creating the output vector using the view's iterators) steps. I just want to point out that a preceding first step would be to filter the input array based on the given conditions as @康桓瑋 points out in a comment. Thanks! – Adham Oct 06 '22 at 07:10
  • 1
    @Adham I added filter while you were writing the comment :-) – cptFracassa Oct 06 '22 at 07:13
  • 1
    Awesome! Hopefully it can help others stuck in the same boat :D. I kinda get now the hype around `ranges` – Adham Oct 06 '22 at 07:23
  • @Jarod42 Guess I didn't read the text well enough. I changed the example now. – cptFracassa Oct 06 '22 at 09:03