I’ve got one type of collection and I want to apply a function to each member of the collection, thereby producing a new collection.
Surely there’s a standard pattern for this?
In JavaScript, it’s called map
:
function getOldValues() { return ["a", "b", "c", "d"]; } var newValues = getOldValues().map(v => v.charCodeAt(0)); // result: [97, 98, 99, 100]
In C#, it’s Select
.
string[] GetOldValues() => new[] { "a", "b", "c", "d" }; var newValues = GetOldValues().Select(v => (int)v[0]).ToArray(); // result: int[] { 97, 98, 99, 100 };
In C++, it’s, um, this clumsy std::transform
.
std::vector<std::string> GetOldValues() { return { "a", "b", "c", "d" }; } auto oldValues = GetOldValues(); std::vector<int> newValues; newValues.reserve(oldValues.size()); std::transform(oldValues.begin(), oldValues.end(), std::back_inserter(newValues), [](auto&& v) { return v[0]; });
It’s clumsy because you need to give a name to the thing being transformed, because you need to call both begin
and end
on it. But giving it a name extends its lifetime, so you end up carrying this oldValues
vector around for no reason.¹
It’s clumsy because you have to construct an empty newValues
and then fill it in.
Would be nice if there were some helper function like
template<typename T, typename U, typename TLambda> T transform_to(U&& u, TLambda&& lambda) { T result; if constexpr (has_size_v<U> && has_reserve_v<T>) { result.reserve(u.size()); } std::transform(u.begin(), u.end(), std::back_inserter(result), std::forward<TLambda>(lambda)); return result; } auto newValues = std::transform_to<std::vector<int>>( GetOldValues(), [](auto&& v) { return v[0]; });
Maybe one exists and I’m missing it? Help me out here.
¹ You can avoid extending the lifetime beyond the transform by pushing it into a lambda:
auto newValues = [&]() { auto oldValues = GetOldValues(); std::vector<int> newValues; newValues.reserve(oldValues.size()); std::transform(oldValues.begin(), oldValues.end(), std::back_inserter(newValues), [](auto&& v) { return v[0]; }); return newValues; }();
but that’s basically just taking the transform_to
function and inlining it as a lambda.
I could write the transforming iterator but if G++ ever gets inner functions it would become mostly obsolete.
In classes that don’t use LINQ at all and if both the source and target are array, I would prefer to use Array.ConvertAll() instead.
That’s just personal preference, and I haven’t measured to see if there is any performance benefit to do so.
Last time I used C++ was some 15 years ago and I was barely proficient with it back then. But I have spent the time using various other languages professionally. A few years ago I thought of checking out C++ again because I found the language fascinating.
But I didn't get very far in because I started feeling rather disillusioned about it. It seems to me that it's bogged down... not even by compatibility, but...
Maybe the Web(kit) Template Framework or the Mozilla Framework Based on Templates would suit you? (If only because you like their initialisms…)
If the element type being transformed from is implicitly convertible to the element type being transformed to (as is the case with 'char' -> 'int', but not 'std::string' -> 'int'), then you don't need std::transform(), you can construct the destination vector from the source vector directly:
auto oldValues = GetOldValues();
std::vector<int> newValues(oldValues.begin(), oldValues.end());
You do still have the lifetime issue with oldValues, but you can reduce its scope to address that, if you don't want to use...
Using C++20 ranges (https://en.cppreference.com/w/cpp/ranges):
auto toCharCode = [](auto&& v) -> int { return v[0]; };
auto oldValues = GetOldValues();
auto newValues = oldValues | std::views::transform(toCharCode);
https://godbolt.org/z/9uq2YD
the catch is that newValues can not be declared std::vector
Although this has the same caveat as
var newValues = GetOldValues().Select(v => (int)v[0]);
In that it will repeat the transform each time it is iterated. We’d need an
actions::to
or similar, to match the trailing
.ToArray()
Wow. I know it has been a decade or so since I have done much C++. But I have tried to keep up on the innovations to the language, but I had no idea that they had essentially added LINQ in C++20. And naturally they decided to overload the bitwise OR to indicate piping rather than some just using normal functions or just keeping with the >> theme from iostreams.
Oh come on, who in their right mind would want c++ for once to follow established practices that work perfectly fine in all other languages most people have experience with if you could instead show everyone how awesome c++’s operator overloading is.
The old joke interview with Stroustroup was prophetic really.
Good thing then that range adaptors can be used as functions, so you can use "follow established practices that work perfectly fine in all other languages most people have experience with". The main downside with them is they preclude internet-bashing. You'll have to make-do with being wrong.
Beside, some functional languages do have similar pipeline operator. Bash and most shell in existence have had them for multiple decades, which should help you be familiar with the...