I recently started exploring C++ concepts (here's a useful tutorial I found: C++ 20 Concepts - A Quick Introduction). Since one of the best ways to learn new programming concepts is by writing actual code, I decided to create a small program that mimics JavaScript's Array.prototype.map
or Rust's .map
iterator (followed by the collect
method) to apply transformations on a vector of numbers.
In this example, we'll use a vector of floats, aiming to obtain a new vector containing the square of each element in the original vector.
Let's start with a very simple program that accomplishes this.
#include <iostream>
#include <vector>
int main()
{
std::vector<double> x{1.0, 2.0, 3.0};
std::vector<double> x_squared;
for (auto x_i : x)
{
x_squared.push_back(x_i * x_i);
}
// Print the squared vector to `stdout`:
std::cout << "x_squared ->";
for (auto x_i : x_squared)
std::cout << " " << x_i;
std::cout << std::endl;
return 0;
}
Now, let’s look at a more advanced way to achieve the same result using features from the C++ Standard Library (from C++17).
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::vector<double> x{1.0, 2.0, 3.0};
std::vector<double> x_squared(x.size());
// Use `std::transform` from the `algorithm` header
// to iterate through `x`, apply our lambda function,
// and store the result in `x_squared`.
std::transform(x.begin(), x.end(), x_squared.begin(), [](double x)
{ return x * x; });
// Print the squared vector to `stdout`:
std::cout << "x_squared ->";
for (auto x_i : x_squared)
std::cout << " " << x_i;
std::cout << std::endl;
return 0;
}
Pretty cool! But let’s now see how we could use C++20 concepts to create a simple API that lets us map
(à la JavaScript, Swift, Rust, etc.) the x
vector into x_squared
.
First, let’s create a simple template that enforces the specified generic type as a number:
template <typename T>
concept CollectorNumber = std::integral<T> || std::floating_point<T>;
Here, I use "collector" as a prefix in my concept name, inspired by Rust’s collect
iterator method (see Processing a Series of Items with Iterators).
With that in place, let's look at the Collector
concept itself:
template <typename Fn, typename T>
concept Collector = CollectorNumber<T> && requires(Fn fn, T v) {
{ fn(v) } -> std::same_as<T>;
};
This concept requires that the callable type Fn
accepts an argument of type T
and returns a result of type T
.
Now, we’re ready to define our template function, collect
, which uses the Collector
concept we created above:
template <typename T, Collector<T> Fn>
std::vector<T> collect(Fn fn, const std::vector<T> &v)
{
std::vector<T> out(v.size());
std::transform(v.begin(), v.end(), out.begin(), fn);
return out;
}
Let’s now use this in a simple program to square x
and store the result in x_squared
, as in our previous examples:
int main()
{
std::vector<double> x{1, 2, 3, 4};
std::vector<double> x_squared = collect([](double x) {
return x * x;
}, x);
// Print the squared vector to `stdout`:
std::cout << "x_squared ->";
for (auto x_i : x_squared)
std::cout << " " << x_i;
std::cout << std::endl;
return 0;
}
That's it!
- The concept-related features used in this program are part of C++20's standard library. If your compiler has trouble compiling, try explicitly including
concepts
,type_traits
, andutility
. - Check out the
main.cc
file below for the full example using ourcollect
template function based on C++20 concepts. - Don’t forget to specify
-std=c++20
when compiling this example.