February 28th, 2020

Should there be a standard C++ pattern for this? transform_to

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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

11 comments

Discussion is closed. Login to edit/delete existing comments.

  • Joshua Hudson

    I could write the transforming iterator but if G++ ever gets inner functions it would become mostly obsolete.

  • cheong00

    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.

  • Valts Sondors

    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...

    Read more
    • Neil Rashbrook

      Maybe the Web(kit) Template Framework or the Mozilla Framework Based on Templates would suit you? (If only because you like their initialisms…)

  • Remy Lebeau

    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...

    Read more
    • Alex Cohn

      the catch is that newValues can not be declared std::vector

    • Adam Rowell

      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()

    • Jeremy Richards

      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.

      • Daniel Sturm

        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.

      • Pierre Baillargeon

        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...

        Read more