Create a vector of pairs from a single vector in C++

Viewed 1333

I have a single even-sized vector that I want to transform into a vector of pairs where each pair contains always two elements. I know that I can do this using simple loops but I was wondering if there is a nice standard-library tool for this? It can be assumed that the original vector always contains an even amount of elements.

Example:

vector<int> origin {1, 2, 3, 4, 5, 6, 7, 8};

vector<pair<int, int>> goal { {1, 2}, {3, 4}, {5, 6}, {7, 8} };
5 Answers

Use Range-v3:

#include <range/v3/range/conversion.hpp>
#include <range/v3/view/transform.hpp>
#include <range/v3/view/chunk.hpp>

using namespace ranges;
using namespace ranges::views;

int main() {
    std::vector<int> origin {1, 2, 3, 4, 5, 6, 7, 8};
    std::vector<std::pair<int, int>> goal {{1, 2}, {3, 4}, {5, 6}, {7, 8}};

    auto constexpr makePairFromRangeOf2 = [](auto two){
        return std::make_pair(two.front(), two.back());
    };

    auto result = origin | chunk(2)
                         | transform(makePairFromRangeOf2)
                         | to_vector;
}

Notice that if you only have to loop on result, then you only need it to be a range, so you can leave | to_vector out, because you'll still be able to do result.begin() and result.end(), which is what makes result a range.

If you don't need the inner containers to truly be std::pairs, but your just happy with calling, say, result.front().front() instead of result.front().first, then you can leave also the transform, and just be happy with auto result = origin | chunk(2);.

You don't mention why you only want a standard solution. However consider that <ranges> is standard in C++20. Unfortunately that feature is not as powerful as pre-C++20 Range-v3 library. But it will be at some point (C++23?), I think without any doubts.

As mentioned by @康桓瑋 , if you're willing to also use the ranges-v3 library, you can use a chunk() view:

std::vector origin = {1, 2, 3, 4, 5, 6, 7, 8};
auto goal = v | ranges::views::chunk(2) | ranges::to<std::vector>;

See it working on GodBolt.

Unlike my other answer, this will be perfectly valid language-wise.

Caveats:

  • This will make a copy of your data!
  • Likely to introduce a bunch of stuff (error strings, exception handlers etc.) into your object code.
  • The ranges library increases compilation times significantly (although perhaps less so with C++20 enabled?)
  • Not based on the standard library - but apparently chunk() and to() will be in C++23, so upto a slight syntax tweak (e.g. adding std::), this will be valid C++23 with only standard library includes.
  • The elements of goal are not std::pairs, bur rather ranges. You will need to get the first and second, or first and last, elements to make an actual pair.

I have a function for handling both even and odd number of elements in a vector. What it does that it takes another parameter to add a number at the end of pair. I don't think there's any standard tool/library to do so as of C++ 20, there is Range-v3 library which will be in C++ 23 which isn't released yet.

Here is the try it online link.

#include <iostream>
#include <vector>

// time complexity: O(n / 2), where `n` is the length of `my_vec`
std::vector<std::pair<int, int>> vec_to_pair(const std::vector<int> &my_vec, int odd_origin)
{
    std::vector<std::pair<int, int>> val;
    for (std::size_t i = 0; i < my_vec.size(); i += 2)
    {
        int sec_val;
        if (i < my_vec.size() - 1)
            sec_val = my_vec[i + 1];
        else if (my_vec.size() % 2 != 0)
            sec_val = odd_origin;
        else 
            break;
        int data[] = {my_vec[i], sec_val};
        val.push_back({data[0], data[1]});
    }
    return val;
}

void print(const std::vector<std::pair<int, int>> &vec)
{
    std::cout << "{ ";
    for (auto &&i : vec)
        std::cout << "{ " << i.first << ", " << i.second << " }  ";
    std::cout << " }" << std::endl;
}

int main(void)
{
    std::vector<int> vec1 = {1, 2, 3, 4, 5};    // odd
    std::vector<int> vec2 = {1, 2, 3, 4, 5, 6}; // even

    auto x1 = vec_to_pair(vec1, -1);
    auto x2 = vec_to_pair(vec2, 0);

    print(x1);
    print(x2);

    return 0;
}

Simple way of doing this without using the range v3 library:

template <typename T>
std::vector<std::pair<T, T>> windowed(const std::vector<T> &vec) {
    const size_t size = vec.size();
    if (size % 2 != 0) {
        throw std::exception("Vector does not contain an even amount of elements!");
    }
    
    std::vector<std::pair<T, T>> result;
    for (size_t i = 0; i < size; i = i + 2) {
        const T &left = vec.at(i);
        const T &right = vec.at(i + 1);
        result.emplace_back(left, right);
    }
    return result;
}

The intuitive, but unfortunately invalid, way to do it

There's a quick-and-dirty approach, which will kinda-hopefully-maybe do what you asked for, and will not even copy the data at all... but the downside is that you can't be certain it will work. It relies on undefined behavior, and can thus not be recommended. I'm describing it because I believe it's what one imagines, intuitively, that we might be able to do.

So, it's about using std::span with re-interpretation of the vector data:

std::vector<int> origin {1, 2, 3, 4, 5, 6, 7, 8};
auto raw_data = reinterpret_cast<std::pair<int, int>*>(origin.data());
std::span<std::pair<int, int>> goal { raw_data, origin.size()/2 };

See this on GodBolt

Caveats:

  • reinterpret_cast is "dirty". It officially results in undefined behavior, and what it does in practice depends on the compiler and platform. You can circumvent this if you also forget about std::pair's, and instead use a 2-dimensional mdspan instead. Of course, mdspan's are not in the standard library.
  • This is C++20. Before C++20 you can still use span's, but they're not in the standard library.
  • Specifically, if you're on a platform where unaligned accesses are not allowed, in may be a problem to insist on there being twice-the-size-of-int values at goal.data().
Related