50

Is it possible to use structured binding with vectors?

E.g.

std::vector<int> vec{1, 2, 3};
auto [a, b, c] = vec;

Above code unfortunately doesn't work (under GCC), but maybe there's a different way (with structured binding) that allows to assign the first three values of a vector to three variables.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
BartekPL
  • 2,290
  • 1
  • 17
  • 34

3 Answers3

56

Structured binding only works if the structure is known at compile time. This is not the case for the vector.

While you do know the structure of the individual elements, you do not know the number of elements, and that is what you are trying to decompose on in your question. Similarly, you can only use structured bindings on array types where the size is known at compile time. Consider:

void f(std::array<int, 3> arr1,
       int (&arr2)[3],
       int (&arr3)[])
{
    auto [a1,b1,c1] = arr1;
    auto [a2,b2,c2] = arr2;
    auto [a3,b3,c3] = arr3;
}

The first two will work, but the last line will fail to compile, because the size of arr3 is not known at compile time. Try it on godbolt.

ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
30

It's easy enough to create a basic wrapper over your vector that gives access to it like a tuple. Since there is indeed no way to retrieve a vector's size at compile time, this throws std::out_of_range if you attempt to destructure too short a vector. Unfortunately I don't know of a way to deduce the number of requested bindings, so that's explicit.

Full code:

#include <string>
#include <vector>
#include <iostream>

template <class T, std::size_t N>
struct vector_binder {
    std::vector<T> &vec;

    template <std::size_t I>
    T &get() {
        return vec.at(I);
    }
};

namespace std {
    template<class T, std::size_t N>
    struct tuple_size<vector_binder<T, N>>
    : std::integral_constant<std::size_t, N> { };

    template<std::size_t I, std::size_t N, class T>
    struct tuple_element<I, vector_binder<T, N>> { using type = T; };
}

template <std::size_t N, class T>
auto dissect(std::vector<T> &vec) {
    return vector_binder<T, N>{vec};
}

int main() {
    std::vector<int> v{1, 2, 3};
    auto [a, b] = dissect<2>(v);

    a = 5;
    std::cout << v[0] << '\n'; // Has changed v through a as expected.
}

Rvalue and const versions of vector_binder as well as better names are left as an exercise to the reader :)

See it live on Coliru

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • 2
    Are you sure `v[0]` should be modified if you do not use `auto&` for the structured binding? If you replace your vector with an array (and change code accordingly), `v[0]` is not modified by the `a = 5` statement. – Holt Jun 28 '18 at 08:13
  • @Holt good question, I'm off reading through the documentation again. It kind of made sense to me when GCC did it but now that I think about it... – Quentin Jun 28 '18 at 08:15
  • Without `&` `v[0]` should *not* be modified. – Hatted Rooster Jun 28 '18 at 08:16
  • @Holt I'm tinkering with it on gcc.godbolt.org, and the latest versions of both GCC and Clang agree to modify `v` through `a`... Dang, I don't have time to write a proper question right now :| – Quentin Jun 28 '18 at 08:20
  • Unless I'm confused, it modifies `v[0]` [by design](http://eel.is/c++draft/dcl.struct.bind#4.sentence-8) – StoryTeller - Unslander Monica Jun 28 '18 at 08:47
  • @StoryTeller I am confused by the discrepancy between this and `std::array` and `std::tuple`. I am actually also very confused by the standard wording regarding this (the whole section... ). – Holt Jun 28 '18 at 08:59
  • 1
    @StoryTeller Oh yeah, I think that's it. The ref on `auto&` applies to the `vector_binder` itself, the elements are accessed by reference through it regardless. You'd have to remove the `&` on `get`'s return type for it to behave otherwise. – Quentin Jun 28 '18 at 09:01
  • 1
    @Quentin But if you remove `&` on `get`, you would not be able to bind to reference. I would be great to really understand what makes this code behave differently than `std::array` or `std::tuple`. – Holt Jun 28 '18 at 10:02
  • It seems that for both tuple and array, you end up in the rvalue overload for `std::get`, so the structured binding ends up producing `int&&` variables. – ComicSansMS Jun 28 '18 at 11:15
  • 4
    @everyone - `vector_binder` is a reference type (a la `string_view`). The fact that we have our own copy of it doesn't matter, it still refers to the underlying vector and the bindings are references into the underlying vector. We're doing `auto __e = dissect<2>(v); __e.get<0>() = 5;` which is effectively `v[0] = 5;` – Barry Jun 28 '18 at 11:39
10

Not ideal since it's more verbose but you can also do:

auto [a, b, c] = array<int, 3>({vec[0], vec[1], vec[2]});

I do not agree with the notion that not knowing the number of elements of a container should prevent structured binding to it's elements. My reasoning is that since the following does not throw a compile time error:

auto a = vec[0];
auto b = vec[1];
auto c = vec[2];

(even though vec[2] might be out of range at run-time), so should be the case for the above structured binding. Meaning, why not leave it to the user to make sure vector has the correct length at runtime, and throw an out of range exception if that is not the case? That is essentially how we use vectors everywhere else in the language.

pooya13
  • 2,060
  • 2
  • 23
  • 29
  • The need to select (once, forever) between undefined behavior (as with `operator[]`) and an exception (as with `at`) is one of the reasons not to support this in the language. – Davis Herring Jan 10 '22 at 14:40