A comment was made on my YouTube channel that destructuring in C++17 would be much more useful if it worked with standard containers. At the moment I responsed that this would be someone impossible to get right without adding too much overhead.
After more thought, however, I started thinking maybe it would be.
What if we could do:
std::vector<int> vec{1,2,3};
auto [x,y,z] = vec;
This could actually be handy. But convincing the language itself to do this would be hard, to say the least. How would it know what operations to perform on which containers? And is this something we really want built into the language?
An adapter of some sort could be employed to perform the transformation from a container into a list of elements that can be destructured by the language. So perhaps we end up with:
std::vector<int> v{1,2,3};
auto [x,y,z] = destructure<3>(v);
We have to tell the compiler how many elements we want (otherwise, how would it know?). The rest can be automatically deduced. The type of container and the contained types.
Here is one option that works (note that this requires at least clang 4, which isn’t officially released at the time of this writing).
template<typename T, size_t ... S>
auto destructure_impl(T &t, std::index_sequence<S...>)
{
return std::forward_as_tuple(*std::next(std::begin(t), S)...);
}
template<size_t Count, typename T>
auto destructure(T &t)
{
return destructure_impl(t, std::make_index_sequence<Count>());
}
We use std::forward_as_tuple
which takes a set of parameters and generates a tuple of references to the values, so we end up with something like std::tuple<int &, int &, int &>
being returned from our destructure
function.
We can further extend it to do an offset and length:
template<size_t Start, typename T, size_t ... S>
auto destructure_impl(T &t, std::index_sequence<S...>)
{
return std::forward_as_tuple(*std::next(std::begin(t), Start+S)...);
}
template<size_t Start, size_t Count, typename T>
auto destructure(T &t)
{
return destructure_impl<Start>(t, std::make_index_sequence<Count>());
}
So a complete example becomes:
#include <cstdint>
#include <array>
#include <vector>
#include <iterator>
#include <functional>
template<typename T, size_t ... S>
auto destructure_impl(T &t, std::index_sequence<S...>)
{
return std::forward_as_tuple(*std::next(std::begin(t), S)...);
}
template<size_t Count, typename T>
auto destructure(T &t)
{
return destructure_impl(t, std::make_index_sequence<Count>());
}
template<size_t Start, typename T, size_t ... S>
auto destructure_impl(T &t, std::index_sequence<S...>)
{
return std::forward_as_tuple(*std::next(std::begin(t), Start+S)...);
}
template<size_t Start, size_t Count, typename T>
auto destructure(T &t)
{
return destructure_impl<Start>(t, std::make_index_sequence<Count>());
}
int main()
{
std::vector<int> v{1,2,3};
auto [x,y] = destructure<1,2>(v);
x = 3;
y = 12;
return v[1];
}
Which compiles down to
main: # @main
pushq %rax
movl $12, %edi
callq _Znwm
movq %rax, %rdi
callq _ZdlPv
movl $3, %eax
popq %rcx
retq
This looks pretty good, but how does it compare to doing it by hand?
#include <cstdint>
#include <array>
#include <vector>
#include <iterator>
int main()
{
std::vector<int> v{1,2,3};
v[1] = 3;
v[2] = 12;
return v[1];
}
Compiled:
main: # @main
pushq %rax
movl $12, %edi
callq _Znwm
movq %rax, %rdi
callq _ZdlPv
movl $3, %eax
popq %rcx
retq
Which is… exactly the same. So we’ve accomplished some level of destructuring support for dynamic containers with 0 extra overhead. Is this something that we should propose for the C++20 standard library?