Chapter 21 - std::transform

Preamble

The std::transform function is a very useful and versatile algorithm.

In a nutshell, the algorithm allows one to apply a function to each element in a range.

Transforming as a concept

Consider the case where we have a vector of doubles corresponding to a series of Celsius values that we want to convert into Fahrenheit values, we might employ code similar to the following:

std::vector<float> tempInCelsius {31.2, 28.1, 3.1, 12.8};
std::vector<float> tempInFahrenheit; 

for (auto celsius: tempInCelsius) {
    float fahrenheit = celsius * (9 / 5) + 32; 
    tempInFahrenheit.push_back(fahrenheit);
}

Though it is often good to abstract the transformation logic applied to each element into its own function:

float toFahrenheit(float celsius) {
    return celsius * (9 / 5) + 32; 
}

int main() {
    std::vector<float> tempInCelsius {31.2, 28.1, 3.1, 12.8};
    std::vector<float> tempInFahrenheit; 

    for (auto celsius: tempInCelsius) {
        tempInFahrenheit.push_back(toFahrenheit(celsius););
    }
}

In the above example, we take each element in tempInCelsius, apply the toFahrenheit function to it, then store the corresponding results in tempInFahrenheit.

In other words, we "transformed" each element in one range into another:

(credits: https://www.fluentcpp.com/2017/02/13/transform-central-algorithm/)

This concept is also referred to as map, or apply-to-all in other languages.

std::transform

The std::transform algorithm is exactly this idea - it allows us to apply a function to each element in a range, and store its results in a separate (or the same) range.

Usage

The basic syntax of the std::transform is as follows:

std::transform(
    <start of range>,
    <end of range>,
    <start of output>,
    <function to apply>
)
  • The first two parameters define a range of elements to apply our function to

  • The third parameter is an iterator that will be used to store the elements; the program will write to *iter and increment it for each element.

  • The fourth parameter is a function that will be applied to each element in the transformation range.

Example

We can rewrite our original example of transforming Celsius to Fahrenheit values:

float toFahrenheit(float celsius) {
    return celsius * (9 / 5) + 32; 
}

int main() {
    std::vector<float> tempInCelsius {31.2, 28.1, 3.1, 12.8};
    std::vector<float> tempInFahrenheit; 
    tempInFahrenheit.resize(tempInCelsius);

    std::transform(
        tempInCelsius.begin(),
        tempInCelsius.end(),
        tempInFahrenheit.begin(),
        toFahrenheit
    )
}

Functionally, the above code behaves exactly the same as the one we've originally written.

On Two Ranges

The std::transform function has a second overload that takes (in essence) two ranges, and applies a binary function that takes two parameters, on each pair of elements taken from the two ranges:

(credits: https://www.fluentcpp.com/2017/02/13/transform-central-algorithm/)

Here is an example that performs a pairwise multiplication addition between two vectors:

int multiply(int a, int b) {
    return a * b;
}

int main() {
    std::vector<int> first {1, 2, 3, 4, 5};
    std::vector<int> second {5, 4, 3, 2, 1};
    std::vector<int> result;
    
    std::transform(
        first.begin(),
        first.end(),
        second.begin(),
        std::back_inserter(result),
        multiply
    );
}

As shown in the diagram, the above snippet will traverse the entirety of the first range, reads the counterpart from the second range, applies the multiply function with the elements from each range, and insert the results into the results vector.

This concept of performing a pairwise enumeration over two collections has the general name called zip.

Transforming In-Place

In the above examples, when we transformed a range, we stored the resulting elements in a separate range.

If no longer care about the original range, we can store the results directly back into the input range - this is called transforming in-place.

int main() {
    std::vector<float> temperatures {31.2, 28.1, 3.1, 12.8};

    std::transform(
        temperatures.begin(),
        temperatures.end(),
        temperatures.begin(),  // Store the results back into the input range 
        toFahrenheit
    )
}

Why use std::transform?

At this point of your C++ journey, there will be little benefit to using std::transform over plain old for-loops, but the potential benefits that you will uncover later on cannot be understated.

Nevertheless, the following are some of the reasons you might decide to start using std::transform now.

Separating concerns of transforming and iterating

With std::transform, it allows you to decompose a for-loop into a function that handles the transformation, and a function call that simply applies the transformation function to each element.

Using iterators ensures a generic interface

Since the parameters for std::transform are iterators, you no longer have to care about how the elements should be inserted, as you would simply call .begin() to obtain an iterator for the output container, regardless of whether it is an std::vector, std::list, or std::set.

If a for-loop was used instead, you would have to check whether to use the .insert() member function, or the .push_back() member function depending on the container used to store the result.

Different execution models

With C++17, you will be able to specify different execution models for std::transform and similar algorithm function calls.

This allows you to apply a transformation to a range in a parallel fashion, potentially speeding up your code immensely.

This is beyond the scope of the course thus far - but good to keep in mind.

Last updated

Was this helpful?