Is it possible to iterate through a vector of vectors columnwise?
Asked Answered
S

3

3

I have a vector of vectors of strings. I want to find the lengths of the longest string in each column. All the subvectors are of the same length and have an element stored in it, so it would be rather easy to find it with two for loops and reversed indices.

vector<vector<string>> myvec = {
                                { "a", "aaa",   "aa"},
                                {"bb",   "b", "bbbb"},
                                {"cc",  "cc",  "ccc"}
                               };

But is it possible to do it with iterators without using indices?

Skyscape answered 27/10, 2022 at 15:20 Comment(6)
for (const auto& vec : myvec) { for (const auto& str : vec) { } } ?Tessitura
why do you want to use iterators? If the inner vectors store columns you can use iterators easilyBickering
Since each sub vector can technically be a different length, not sure how an iterator would generically work in that situation.Taffeta
What you definitely can do is, write a class for 1d vector which you manipulate as 2d vector. Then write an iterator which will be interested col wise or raw wise... Currently no other way... Or may be iterator loop and patellel indexingPractice
@Tessitura That would be iterating row-wise, not column-wise.Margarito
You should seriously consider whether a vector is the right data structure, though, as column iteration is very expensive - no data localityVolnak
O
1

Here is a solution using C++20 ranges and lambdas that is similar to Nelfeals answer:

// returns a function returning the i-th element of an iterable container
auto ith_element = [](size_t i) {
    return [i](auto const& v){
        return v[i];
    };
};

// returns a range over the i-th column
auto column = [ith_element](size_t i) {
    return std::views::transform(ith_element(i)); // returns a range containing only the i-th elements of the elements in the input range
};

// returns the size of something
auto length = [](auto const& s){ return s.size(); };

// returns the max length of the i-th column
auto max_length_of_col = [column, length](auto const& v, size_t i) {
    return std::ranges::max(
        v | column(i) | std::views::transform(length)
    );
};

I personally like how the ranges library helps you convey intent with your code, rather than having to prescribe the procedure to achieve your goal.

Note that if you replace the body of inner lambda in ith_element with the following block, it will also work for iterable containers without random access.

auto it = v.cbegin();
std::ranges::advance(it, i);
return *it

Demo

As a final remark, this solution lets you iterate over one column given an index of the column. I would advise against implementing a column iterator for vector<vector>: The existence of an iterator implies that something exists in memory that you can iterate over. The existence of columns is only implied by the additional information that you have given us, namely that all rows have the same length. If you do want iterators both for columns and rows, I would wrap your container in a new type (usually called matrix or similar) that properly conveys that intent. Then you can implement iterators for that new type, see Calath's answer.

EDIT:

I realized that my argument against a column iterator can be used as an argument against the column function in this answer as well. So here is a solution that let's you iterate over columns in a range-based for loop intead of iterating over column indices:

for (auto column : columns(myvec)){
    std::cout << max_length(column) << std::endl;
}
Omit answered 27/10, 2022 at 22:51 Comment(5)
I tried your edited solution. It compiles but I get total different output with my gcc 10.2.1 which makes little sense to me.Skyscape
Interesting. 11.2 seems to be the lowest gcc version that works. Not sure what the problem is, but the fact, that it works in newer versions makes it seem like a compiler bug to me...my original answer works with gcc 10.2, so maybe it has something to do with the ith_element implementation or the use of functions rather than lambdas...I would have to investigate.Omit
Here is a working version: godbolt.org/z/jsa4sWqEe. The problem seems to come from the ith_element implementation. I still don't know exactly what the problem with the ith_element implementation is, but at least here you have a working version. I will update the answer accordinglyOmit
See here: https://mcmap.net/q/999181/-bug-or-compilation-error-with-some-compilers-for-simple-std-ranges-code/12173376Omit
I updated the answer once more according the above mentioned SO question/answers. Now it works with more compilers and is more general, since it does not rely on the existence of the random access operator[](size_t).Omit
E
6

Assuming the inner vectors are all the same length, you can have something like

template <typename T>
class columnwise {
    std::vector<std::vector<T>> * underlying;
    struct proxy {
        std::vector<std::vector<T>> * underlying;
        std::vector<T>::difference_type offset;
        struct iterator {
            std::vector<std::vector<T>>::iterator outer;
            std::vector<T>::difference_type offset;
            using reference = typename std::vector<T>::reference;
            using pointer = typename std::vector<T>::pointer;
            iterator operator++() { ++outer; return *this; }
            reference operator*() { return *(outer->begin() + offset); }
            pointer operator->() { return (outer->begin() + offset).operator->(); }
            bool operator==(iterator rhs) { return (outer == rhs.outer) && (offset == rhs.offset); }
        };
    public:
        iterator begin() { return { underlying->begin(), offset }; }
        iterator end() { return { underlying->end(), offset }; }
    };
    struct iterator {
        // member type aliases
        std::vector<std::vector<T>> * underlying;
        std::vector<T>::difference_type offset;
        iterator operator++() { ++offset; return *this; }
        proxy operator*() { return { underlying, offset }; }
        bool operator==(iterator rhs) { return (underlying== rhs.underlying) && (offset == rhs.offset); }
    };
    std::vector<T>::difference_type inner_size() { if (auto it = underlying->begin(); it != underlying->end()) { return it->size(); } return 0; }
public:
    columnwise(std::vector<std::vector<T>> & vec) : underlying(&vec) {}
    iterator begin() { return { underlying, 0 }; }
    iterator end() { return { underlying, inner_size() }; }
};

Which iterates as you expect.

Ellie answered 27/10, 2022 at 16:11 Comment(0)
F
3

What about something like this?

std::vector<std::size_t> max_lengths(myvec.front().size(), 0);
for (auto const& strings : myvec) {
    std::transform(max_lengths.begin(), max_lengths.end(), strings.begin(), max_lengths.begin(),
        [](std::size_t l, std::string const& s) {
            return std::max(l, s.size());
        }
    );
}

Demo

Finney answered 27/10, 2022 at 15:59 Comment(0)
O
1

Here is a solution using C++20 ranges and lambdas that is similar to Nelfeals answer:

// returns a function returning the i-th element of an iterable container
auto ith_element = [](size_t i) {
    return [i](auto const& v){
        return v[i];
    };
};

// returns a range over the i-th column
auto column = [ith_element](size_t i) {
    return std::views::transform(ith_element(i)); // returns a range containing only the i-th elements of the elements in the input range
};

// returns the size of something
auto length = [](auto const& s){ return s.size(); };

// returns the max length of the i-th column
auto max_length_of_col = [column, length](auto const& v, size_t i) {
    return std::ranges::max(
        v | column(i) | std::views::transform(length)
    );
};

I personally like how the ranges library helps you convey intent with your code, rather than having to prescribe the procedure to achieve your goal.

Note that if you replace the body of inner lambda in ith_element with the following block, it will also work for iterable containers without random access.

auto it = v.cbegin();
std::ranges::advance(it, i);
return *it

Demo

As a final remark, this solution lets you iterate over one column given an index of the column. I would advise against implementing a column iterator for vector<vector>: The existence of an iterator implies that something exists in memory that you can iterate over. The existence of columns is only implied by the additional information that you have given us, namely that all rows have the same length. If you do want iterators both for columns and rows, I would wrap your container in a new type (usually called matrix or similar) that properly conveys that intent. Then you can implement iterators for that new type, see Calath's answer.

EDIT:

I realized that my argument against a column iterator can be used as an argument against the column function in this answer as well. So here is a solution that let's you iterate over columns in a range-based for loop intead of iterating over column indices:

for (auto column : columns(myvec)){
    std::cout << max_length(column) << std::endl;
}
Omit answered 27/10, 2022 at 22:51 Comment(5)
I tried your edited solution. It compiles but I get total different output with my gcc 10.2.1 which makes little sense to me.Skyscape
Interesting. 11.2 seems to be the lowest gcc version that works. Not sure what the problem is, but the fact, that it works in newer versions makes it seem like a compiler bug to me...my original answer works with gcc 10.2, so maybe it has something to do with the ith_element implementation or the use of functions rather than lambdas...I would have to investigate.Omit
Here is a working version: godbolt.org/z/jsa4sWqEe. The problem seems to come from the ith_element implementation. I still don't know exactly what the problem with the ith_element implementation is, but at least here you have a working version. I will update the answer accordinglyOmit
See here: https://mcmap.net/q/999181/-bug-or-compilation-error-with-some-compilers-for-simple-std-ranges-code/12173376Omit
I updated the answer once more according the above mentioned SO question/answers. Now it works with more compilers and is more general, since it does not rely on the existence of the random access operator[](size_t).Omit

© 2022 - 2024 — McMap. All rights reserved.