Skip to content

Instantly share code, notes, and snippets.

@BB-301
Last active November 10, 2024 15:15
Show Gist options
  • Save BB-301/b4cba11cfd4991339a60e4468b94bc2d to your computer and use it in GitHub Desktop.
Save BB-301/b4cba11cfd4991339a60e4468b94bc2d to your computer and use it in GitHub Desktop.
Mimicking JavaScript's Array.prototype.map in C++20 with Concepts for Transforming Number Vectors

Implementing Number Array Mapping Using C++20 Concepts

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.

Program #1

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

Program #2

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

Squaring the Vector Using C++20 Concepts

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:

Program #3

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!

Concluding Words

  • 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, and utility.
  • Check out the main.cc file below for the full example using our collect template function based on C++20 concepts.
  • Don’t forget to specify -std=c++20 when compiling this example.
#include <iostream>
#include <sstream>
#include <vector>
// NOTE: The concept-related facilities used in this
// program all appear to be part of C++20's standard
// library prelude, but you can uncomment those includes
// if the program is not compiling for you.
// #include <concepts>
// #include <type_traits>
// #include <utility>
template <typename T>
void print_vector(const std::vector<T> &v)
{
std::stringstream ss;
ss << "[";
for (auto x : v)
{
ss << " " << x;
}
ss << " ]";
std::cout << ss.str();
}
template <typename T>
concept CollectorNumber = std::integral<T> || std::floating_point<T>;
template <typename Fn, typename T>
concept Collector = CollectorNumber<T> && requires(Fn fn, T v) {
{
fn(v)
} -> std::same_as<T>;
};
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;
}
int main()
{
std::vector<double> x{1, 2, 3, 4};
std::vector<double> x_squared = collect([](double x)
{ return x * x; },
x);
std::cout << "My vector:" << std::endl;
print_vector(x);
std::cout << std::endl;
std::cout << "My squared vector:" << std::endl;
print_vector(x_squared);
std::cout << std::endl;
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment