24

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} };
Darth-CodeX
  • 2,166
  • 1
  • 6
  • 23
Schottky
  • 1,549
  • 1
  • 4
  • 19
  • @DrewDormann sry, I don't quite understand. Where do you see "two vectors"? @Darth-CodeX What I meant is that in my use-case, `origin` always contains an even amount of elements – Schottky Feb 14 '22 at 13:38
  • 1
    @Schottky OOH now I got it – Darth-CodeX Feb 14 '22 at 13:40
  • If you made an iterator that advanced twice you could use std::transform easily enough – Taekahn Feb 14 '22 at 13:41
  • @DrewDormann Right, sorry about that. I changed it! – Schottky Feb 14 '22 at 13:48
  • @Taekahn: But OP is looking for an existing tool, not something to write on their own. – einpoklum Feb 14 '22 at 14:03
  • @einpoklum last time I checked, iteratiors and transform are existing std tools. – Taekahn Feb 14 '22 at 14:08
  • 4
    Is there a need for it to be a standard tool ready on the shelf for such a simple task ? As you can see [here](https://godbolt.org/z/P5M6GdEMY), a simple single `for` loop does the trick. Why making things complicated (as proposed answers are) when they can be so simple ? – Fareanor Feb 14 '22 at 15:37

5 Answers5

21

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.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 1
    OP asked for a "standard library tool". – einpoklum Feb 14 '22 at 14:01
  • Ooooops. I'll put a note about it. – Enlico Feb 14 '22 at 14:03
  • Hmm and you [can't use std::ranges](https://stackoverflow.com/questions/66928472/c20-how-to-split-range-by-size) either – Alan Birtles Feb 14 '22 at 14:06
  • 7
    In fact, [C++23 has adopted `ranges::to` and `views::chuck`](https://en.cppreference.com/w/cpp/compiler_support), so this is *actually* the "standard" answer. – 康桓瑋 Feb 14 '22 at 14:11
  • I really like this solution. The reason I asked for a standard tool is that I'm on a bigger project where it might be hard to add other libraries. I'm still going to accept this answer for now since it's pretty much what I asked for. I suppose a purely standard library approach is not possible. – Schottky Feb 14 '22 at 14:26
  • 4
    https://godbolt.org/z/GsqWG1WW7 – Marek R Feb 14 '22 at 14:38
  • @Darth-CodeX I like your answer and have upvoted it. However, I explicitly asked for anything that I don't have to write myself because it might already exist. – Schottky Feb 14 '22 at 15:37
13

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 = origin | 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.
einpoklum
  • 118,144
  • 57
  • 340
  • 684
6

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;
}
Darth-CodeX
  • 2,166
  • 1
  • 6
  • 23
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;
}
Tohnmeister
  • 468
  • 1
  • 5
  • 14
-9

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::vector's and std::pair's, and instead use a 2-dimensional std::mdspan for the result. However - std::mdspan's only enter the language in C++20.
  • Speaking of language standard, this is C++20, because of std::span. Before C++20 you can still use span's (and mdspan's), but from (popular) independent libraries.
  • 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().
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 4
    The caveat shouldn’t be that it “often” results in undefined behaviour but rather that *it does so **here***. The code is simply illegal: a `std::pair` may not alias contiguous `int`s. – Konrad Rudolph Feb 14 '22 at 14:04
  • 2
    @KonradRudolph: Rephrased. "illegal", however, is not an appropriate description either. The C++ police will not come arrest you for it and bring you to stand trial for your crimes. – einpoklum Feb 14 '22 at 14:06
  • 4
    With the way C++ is, I don't think it's reasonable recommending UB regardless. – Passer By Feb 14 '22 at 14:14
  • 5
    The word “illegal” is routinely used to describe C++ code that violates the standard, in particular code which exhibits UB, and it’s a completely fine description. I didn’t downvote this answer but I also don’t think it’s a good answer: UB in general is *not OK*. It may be OK in very specific circumstances where you know the compiler and platform inside out. In general it isn’t. I almost never know the compiler/platform well enough to be confident that I can rely on UB without messing up. – Konrad Rudolph Feb 14 '22 at 14:16
  • @PasserBy: I didn't recommend it. But - let me clarify that point. – einpoklum Feb 14 '22 at 14:23
  • @KonradRudolph: Clarified that this can't be recommended. I still think this is an important answer because of its intuitiveness. – einpoklum Feb 14 '22 at 14:28
  • 3
    I fail to see what is intuitive about `reinterpret_cast`. – rubenvb Feb 14 '22 at 14:37
  • @einpoklum, why not including this answer as a tail to the other one, and deleting this? If you think the info is useful, better to put it together with the "good" answer. – Enlico Feb 14 '22 at 16:30
  • @Enlico: Because it's a different answer. Also, I don't agree with the vote count the different answers got. – einpoklum Feb 14 '22 at 18:51
  • @rubenvb: It's so intuitive, that there's an idiom in dictionary about this situation: [Six of one, half a dozen of the other](https://www.merriam-webster.com/dictionary/it's%20six%20of%20one,%20half%20(a)%20dozen%20of%20the%20other). – einpoklum Feb 14 '22 at 18:51
  • "Intuitive"? No. "Recommended". Certainly not. "Cute and clever". Absolutely. Downvoting because it's not a good answer to the question, but I certainly do respect the craft. – Silvio Mayolo Feb 14 '22 at 22:04
  • 1
    @SilvioMayolo: Deny it all you want, but it is perfectly intuitive to reinterpret a long sequence of element as a sequence of pairs of elements. – einpoklum Feb 14 '22 at 22:14
  • This is my favorite answer although ranges produce very sharp answers to the question – NGI Feb 15 '22 at 23:23
  • Undefined behaviour and intuitive cannot live in the same sentence. The thing you think you are using intuitively does not do what you think it should intuitively do (at least not guaranteed by the language). It's undefined behaviour for a reason: there will be cases this goes horribly wrong. – rubenvb Feb 16 '22 at 15:46
  • @rubenvb: "The thing you think you are using intuitively does not do what you think it should intuitively do" <- Ah, but it does, on non-esoteric machines. Intuition is not about language-lawyer guarantees. – einpoklum Feb 21 '22 at 14:08