Almost all problems in the history of programming require some form of data sequence manipulation. With std::transform
, you can convert containers into different data. In this post, we look at how to use this standard library function.
Interestingly, this post is part of a series of C++ standard library posts, where we look into popular functions in the standard, read the documentation, and look at usage examples. If you like this post, check out how to use std::find_if
to find elements in C++ containers.
Video Lesson – How std::transform
Works In C++ With Examples
for the purposes of showing useful examples, the video below explains how to use std::transform
to modify C++ vector, arrays, lists, maps or any other C++ container out there.
Interestingly, we look at how to transform an array of People
information (including name, weight, etc) into a vector of BMIs (body mass index).
Purpose Of std::transform
And Why You Should Use It In C++ To Convert Containers
Modifying or transforming big chunks of data is nothing new. In fact, all codebases I’ve worked with in the past required transforming vectors of a certain data type into another vector of a different type.
For this reason, std::trasnform
is perfect for that specific problem: turning a container of a particular type into another container of a different type in C++.
For example, you may have an array of images that you want to pass through a fancy (perhaps machine learning) algorithm and deduce object labels. Therefore, the solution to this problem is obvious here. You want to transform your list of images into a list of labels.
And if it’s not yet clear, std::transform
does just that.
Documentation & Usage
Before you use the function, you should probably take a look at the standard library’s transform documentation.
Specifically, you can notice that there are different ways you can call std::transform
. The list below summarises the main calling conventions from the documentation page.
- Transforming a single container into another. In this calling mode,
std::transform
traverses a single container and converts each element into a different type. - Transforming two containers into another container. Similarly, this calling mode makes
std::transform
traverse two containers jointly. The results of the conversion will still be added to a single container.
In addition, there are different signatures for each calling mode. For example, there are now constexpr
alternatives to std::transform
.
Using std::transform
To Change C++ Vectors
In this section, we will look at the actual usage of std::transform
in the context of modifying our own data types.
More specifically, consider the following struct and array in the code block below. Needless to say, we will be transforming the container below into something else.
struct Person
{
float height;
float weight;
int age;
std::string_view name;
};
static constexpr std::array<Person, 8> people {{
{1.7f, 60.2f, 24, "matheus"},
{1.9f, 99.1f, 22, "lucas"},
{1.63f, 57.12f, 27, "linda"},
{1.98f, 115.3f, 41, "kobe"},
{1.82f, 75.5f, 41, "jake"},
{1.65f, 52.2f, 37, "jena"},
{1.78f, 110.3f, 57, "patrick"},
{1.63f, 123.5f, 47, "drew"}
}};
In addition, the code above requires the headers <array>
and <string_view>
to work.
Transforming Our C++ Person Array Into An Array Of BMIs
Now that we’ve got the necessary context for our std::transform
exercise, let’s put the function to use. Firstly, let’s create our BMI struct, which will be the type we will convert Person
elements into.
struct PersonBmi
{
float bmi;
std::string_view name;
};
Assuming that the previously created structs and array were added to a C++ file, let’s write code to turn our Person
array into a BMI in the main function.
int main()
{
std::vector<PersonBmi> bmis;
std::transform(begin(people),
end(people),
back_inserter(bmis),
[](auto const& person){
return PersonBmi{
person.weight / (person.height * person.height),
person.name
};
});
}
Voilà, you now have a vector of PersonBmi
elements in bmis
. Unexpectedly, each element in bmis
corresponds to the same positional element in the people people
array. Just note that you need to include <algorithm>
to be able to run std::transform
.
How Does Transform Work? What Types Of Arguments Does It Need? How Does It Convert Containers?
Unsurprisingly, since std::transform
is a function that operates on containers, it requires iterators to be passed in.
If you don’t know what C++ iterators are, make sure you read about them online as they are an essential part of the standard library. However, they are objects that point to elements at a position in a C++ container. They can also represent ranges if two iterators refer to elements in the same container.
In simple terms, std::transform
will go through each element in the input container, run a conversion function, then save the return type of the conversion into a destination array.
Input Arguments To std::transform
The list below describes what each argument is and what they represent.
- The first argument represents an input iterator. In other words, this is the iterator to the first element that
std::transform
will traverse. For most cases, you will probably want to usebegin(container)
that will get the first iterator of yourcontainer
. - On the other hand, the second argument represents the last element to traverse with
std::transform
. This is also an input iterator, which must refer to the same type as the first iterator. Note thatstd::transform
will not include the element referenced by this iterator when traversing the container. Similarly, you will probably want to useend(container)
. Note thatend(container)
will get the element after the last element ofcontainer
, meaning thatstd::transform
will traverse the last element. - The third argument is an output iterator. Specifically, this output iterator refers to where the conversion results will be stored, or the first position of the destination container. Given that most people will be transforming vectors,
back_inserter(container)
will get you the output iterator that will always insert at the back. - Lastly, we have either a function pointer, a functor, or a lambda as the last argument. This is the conversion function that
std::transform
will execute on each element of the input array. Due to this behavior, this function object has to take in one argument of the same type as the input container. In addition, it must return the same type as the destination container!
Running std::transform
With Two Different Input Containers In C++
Before we end the post, let’s also look at the other signature of std::transform
. Specifically, the version of std::transform
that takes two input arrays.
Let’s say you have two containers with the same number of elements. In addition, you want to combine or convert each corresponding element into a single value in a destination array.
For the purposes of this post, we will replicate Python’s zip
function. If you’re a Python programmer, you know that zip
takes two lists and turns them into one list, where each element is a pair of elements in the first list, combined with the corresponding elements in the second.
The code below replicates this function with the use of templates and std::transform
‘s less-known two inputs overload.
template <typename T, typename U>
std::vector<std::pair<T, U>> zip(std::vector<T> const& A, std::vector<U> const& B)
{
std::vector<std::pair<T, U>> results;
std::transform(begin(A),
end(A),
begin(B),
back_inserter(results),
[](auto const& a, auto const& b){
return std::make_pair(a, b);
});
return results;
}
Although the zip
function above only works on vectors, calling it with two different vectors will return a vector where each element is a pair formed with the corresponding positional elements in the first and second input vectors.
Differences Between 1 vs 2 Input Containers
Essentially, the only differences are the extra input iterator (or begin(B)
in the code above), and the extra argument that the conversion function has to take.
The extra input iterator is the first element on the second container. Interestingly, everything works fine if the two input containers have the same size. But what happens if they dont? Comment below and I’ll give you a shoutout if you get the right answer!
Similarly, the extra argument to the conversion function (or lambda in our case) is the element that comes from the additional container. Hence it must be the same type as the elements in our second container!
Advantages & Disadvantages Of Using std::transform
To Convert Containers
There aren’t many problems with std::transform
itself. However, using std::transform
requires us to create the destination container in the first place, such as an empty vector to store our conversions into.
This is, in my opinion, not very elegant as we can’t const
that output container, and hence make our code a little more prone to coding errors down the line. However, the alternatives with loops still require the same thing.
Personally, I prefer using std::transform
when I can to make the intent of the code more visible. In addition, looking at a call to std::transform
immediately tells you what is going on, in terms of what container you’re modifying, the conversion function, and where the results are stored. You just don’t get the same visual niceness with raw loops.
Have I missed anything? Do you have any questions or feedback? Feel free to comment below!
Be First to Comment