Combining n vectors into one vector of n-tuples

457 Views Asked by At

I'm thinking about a function with signature

template<typename ...Ts>
std::vector<std::tuple<Ts...>> join_vectors(std::vector<Ts>&&...) {
    //...
};

but probably a more general one accepting any iterable instead of just std::vector would be good. Probably it would have a signature like this?

template<template<typename> typename C, typename ...Ts>
C<std::tuple<Ts...>> join_vectors(C<Ts>&&...) {
    // ...
};

However, I'm not at this level yet in C++ (despite doing the same in Haskell would be relatively easy), hence I seek for help.

Unfortunately, Range-v3's zip is not at my disposal in this case. I'm tagging it because I think those interested in it are in a better position to help me.

1

There are 1 best solutions below

11
On BEST ANSWER

For any indexable containers with size something like this is possible:

#include <tuple>
#include <vector>
#include <algorithm>

// Copy from lvalue containers, move from rvalue containers.
template<typename ...Cs>
auto zip(Cs... vecs) {
    std::vector<std::tuple<typename std::decay_t<Cs>::value_type...>> vec;

    auto len = std::min({vecs.size()...});
    vec.reserve(len);
    for(std::size_t i=0;i<len;++i){
        vec.emplace_back(std::move(vecs[i])...);
    }
    return vec;
};

//Return vector of tuples with & for non-const vecs and const& if const.
template<typename ...Cs>
auto zip_view(Cs&... vecs) {
    std::vector<std::tuple<decltype(vecs[0])...>> vec;
    auto len = std::min({vecs.size()...});
    vec.reserve(len);
    for(std::size_t i=0;i<len;++i){
        vec.emplace_back(vecs[i]...);
    }
    return vec;
};

If the containers have properly implemented move constructors, this solution will copy the containers passed as lvalues and move from rvalue ones. Very slight downside is that lvalue containers are copied whole first instead of only the individual elements.

Example [Godbolt]

#include <iostream>
#include <memory>
template<typename T, typename...Args>
void print_tuple(const T& first, const Args&... args){
    std::cout<<'('<<first;
    ((std::cout<<','<< args),...);
    std::cout<<')';
}

template<typename T>
struct helper{
using fnc_t = void;
};
template<typename...Args>
struct helper<std::tuple<Args...>>{
using fnc_t = void(*)(const Args&... args);
};
template<typename...Args>
struct helper<std::tuple<Args&...>>{
using fnc_t = void(*)(const Args&... args);
};

template<typename T>
using fnc_t2 = typename helper<T>::fnc_t;

template<typename T>
void template_apply(fnc_t2<T> f, const T& tuple){
    std::apply(f, tuple);
}

template<typename T>
void print_vec(const std::vector<T>& vec){
    for(const auto&e:vec){
        template_apply(print_tuple,e);
        std::cout<<'\n';
    }
}
struct MoveOnlyFoo{
    MoveOnlyFoo(int i):m_i(i){}

    int m_i;
    std::unique_ptr<int> ptr = nullptr;
};
    std::ostream& operator<<(std::ostream& o, const MoveOnlyFoo& foo){
        return o<<foo.m_i;
    }


int main(){
    std::vector v1{1,2,3,4,5,6};
    std::vector v2{'a','b','c','d','e'};
    std::vector v3{1.5,3.5,7.5};
    std::vector<MoveOnlyFoo> vmove;
    vmove.emplace_back(45);
    vmove.emplace_back(46);
    vmove.emplace_back(47);
    const std::vector v4{-1,-2,-3,-4,-5};

    //Move rvalues, copy lvalue.
    print_vec(zip(v1,v2,v3, v4, std::move(vmove)));
    // This won't work since the elements from the last vector cannot be copied.
    //print_vec(zip(v1,v2,v3, v4, vmove));
    std::cout<<"View:\n";
    //View, provides const& for const inputs, & for non-const
    print_vec(zip_view(v1,v2,v3,v4));
    std::cout<<"Modify and print:\n";
    for(auto& [x,y]: zip_view(v1,v2)){
        ++x,++y;
    }
    // Note the view can work with const containers, returns tuple of `const T&`.
    print_vec(zip_view(std::as_const(v1),std::as_const(v2)));
}

Output

(1,a,1.5,-1,45)
(2,b,3.5,-2,46)
(3,c,7.5,-3,47)
View:
(1,a,1.5,-1)
(2,b,3.5,-2)
(3,c,7.5,-3)
Modify and print:
(2,b)
(3,c)
(4,d)
(5,e)
(6,f)

Please disregard the readability of the printing code ;)

I modeled it after python zip functionality. Note your initial proposal copies the vectors, so the output is a vector with the values moved from the parameters.

Returning an iterable Cs is harder because you would have to specify how to insert elements into it, iterators cannot do it on their own.

Getting it work with iterators (but returning still a vector) is a chore, but in theory also possible.