结构化绑定和引用元组

时间:2018-04-03 10:59:33

标签: c++ c++17

我在设计一个简单的zip函数时遇到了一个问题,可以用这种方式调用:

for (auto [x, y] : zip(std::vector{1,2,3}, std:vector{-1, -2, -3}) {
    // ...
}

因此zip会返回zip_range类型的对象,它本身会显示beginend函数返回zip_iterator

现在,我实现它时zip_iterator使用std::tuple<Iterators> - 其中Iterators是压缩容器迭代器的类型 - 以保持其在压缩容器中的位置。当我取消引用zip_iterator时,我获得了对压缩容器元素的引用元组。问题是它不适合结构化绑定语法:

std::vector a{1,2,3}, b{-1, -2, -3};
for (auto [x, y] : zip(a, b)) { // syntax suggests by value
    std::cout << ++x << ", " << --y << '\n'; // but this affects a's and b's content
}

for (auto& [x, y] : zip(a, b)) { // syntax suggests by reference
    // fails to compile: binding lvalue ref to temporary
}

所以我的问题是:你能看到一种方法来协调这个引用元组的实际类型(临时值)和它的语义(左值,允许修改它所引用的内容)吗?

我希望我的问题不是太宽泛。这是一个工作示例,使用clang++ prog.cc -Wall -Wextra -std=gnu++2a进行编译(由于gcc处理演绎指南的方式存在错误,因此无法使用gcc):

#include <tuple>
#include <iterator>
#include <iostream>
#include <vector>
#include <list>
#include <functional>


template <typename Fn, typename Argument, std::size_t... Ns>
auto tuple_map_impl(Fn&& fn, Argument&& argument, std::index_sequence<Ns...>) {
    if constexpr (sizeof...(Ns) == 0) return std::tuple<>(); // empty tuple
    else if constexpr (std::is_same_v<decltype(fn(std::get<0>(argument))), void>) {
        [[maybe_unused]]
        auto _ = {(fn(std::get<Ns>(argument)), 0)...}; // no return value expected
        return;
    }
    // then dispatch lvalue, rvalue ref, temporary
    else if constexpr (std::is_lvalue_reference_v<decltype(fn(std::get<0>(argument)))>) {
        return std::tie(fn(std::get<Ns>(argument))...);
    }
    else if constexpr (std::is_rvalue_reference_v<decltype(fn(std::get<0>(argument)))>) {
        return std::forward_as_tuple(fn(std::get<Ns>(argument))...);
    }
    else {
        return std::tuple(fn(std::get<Ns>(argument))...);
    }
}

template <typename T>
constexpr bool is_tuple_impl_v = false;

template <typename... Ts>
constexpr bool is_tuple_impl_v<std::tuple<Ts...>> = true;

template <typename T>
constexpr bool is_tuple_v = is_tuple_impl_v<std::decay_t<T>>;


template <typename Fn, typename Tuple>
auto tuple_map(Fn&& fn, Tuple&& tuple) {
    static_assert(is_tuple_v<Tuple>, "tuple_map implemented only for tuples");
    return tuple_map_impl(std::forward<Fn>(fn), std::forward<Tuple>(tuple),
                          std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>());
}

template <typename... Iterators>
class zip_iterator {
    public:
    using value_type = std::tuple<typename std::decay_t<Iterators>::value_type...>;
    using difference_type = std::size_t;
    using pointer = value_type*;
    using reference = value_type&;
    using iterator_category = std::forward_iterator_tag;

    public:
    zip_iterator(Iterators... iterators) : iters(iterators...) {}
    zip_iterator(const std::tuple<Iterators...>& iter_tuple) : iters(iter_tuple) {}
    zip_iterator(const zip_iterator&) = default;
    zip_iterator(zip_iterator&&) = default;

    zip_iterator& operator=(const zip_iterator&) = default;
    zip_iterator& operator=(zip_iterator&&) = default;

    bool operator != (const zip_iterator& other) const { return iters != other.iters; }

    zip_iterator& operator++() { 
        tuple_map([](auto& iter) { ++iter; }, iters);
        return *this;
    }
    zip_iterator operator++(int) {
        auto tmp = *this;
        ++(*this);
        return tmp;
    }
    auto operator*() {
        return tuple_map([](auto i) -> decltype(auto) { return *i; }, iters);  
    }    
    auto operator*() const {
        return tuple_map([](auto i) -> decltype(auto) { return *i; }, iters);
    }
    private:
    std::tuple<Iterators...> iters;
};

template <typename... Containers>
struct zip {
    using iterator = zip_iterator<decltype(std::remove_reference_t<Containers>().begin())...>;
    template <typename... Container_types>
    zip(Container_types&&... containers) : containers_(containers...) {}
    auto begin() { return iterator(tuple_map([](auto&& i) { return std::begin(i); }, containers_)); }
    auto end()   { return iterator(tuple_map([](auto&& i) { return std::end(i); },   containers_)); }
    std::tuple<Containers...> containers_;
};

template <typename... Container_types>
zip(Container_types&&... containers) -> zip<std::conditional_t<std::is_lvalue_reference_v<Container_types>,
                                                             Container_types,
                                                             std::remove_reference_t<Container_types>>...>;

int main() {

    std::vector a{1,2,3}, b{-1, -2, -3};

    for (auto [x, y] : zip(a, b)) { // syntax suggests by value
        std::cout << x++ << ", " << y-- << '\n'; // but this affects a's and b's content
    }
    for (auto [x, y] : zip(a, b)) { 
        std::cout << x << ", " << y << '\n'; // new content
    }
    //for (auto& [x, y] : zip(a, b)) { // syntax suggests by reference
        // fails to compile: binding lvalue ref to temporary
    //}

}

2 个答案:

答案 0 :(得分:2)

从技术上讲,这不是一个结构化绑定问题,而是一个参考语义类型问题。 auto x = y看起来像是在复制,然后在一个独立的类型上运行,对tuple<T&...>(以及reference_wrapper<T>string_view以及{{1}等类型来说绝对不是这种情况} 和别的)。

然而,正如T.C.在评论中建议,你可以做一些可怕的事情来完成这项工作。请注意,实际上并不这样做。我认为你的实施是正确的。但仅仅是为了完整性。一般兴趣。

首先,结构化绑定的措辞表明基于underlying object的值类别调用span<T>的方式不同。如果它是左值引用(即get()auto&),则在左值上调用auto const&。否则,它在xvalue上调用。我们需要通过制作:

来利用这一点
get()

做一件事,

for (auto [x, y] : zip(a, b)) { ... }

做点别的事。其他东西需要首先实际编译。为此,您的for (auto& [x, y] : zip(a, b)) { ... } 需要返回左值。要做那个,它实际上需要在其中存储zip_iterator::operator*。最简单的方法(在我看来)是存储tuple<T&...>并让optional<tuple<T&...>>对其进行operator*并返回其emplace()。那就是:

value()

但这仍然让我们想要不同的template <typename... Iterators> class zip_iterator { // ... auto& operator*() { value.emplace(tuple_map([](auto i) -> decltype(auto) { return *i; }, iters)); return *value; } // no more operator*() const. You didn't need it anyway? private: std::tuple<Iterators...> iters; using deref_types = std::tuple<decltype(*std::declval<Iterators>())...>; std::optional<deref_types> value; }; s。为了解决这个问题,我们需要自己的get()类型 - 它提供了自己的tuple,这样当调用左值时它会产生左值,但是当在xvalue上调用它时会产生prvalues。

我认为是这样的:

get()

在非左值参考的情况下,这意味着我们将一堆rvalue references绑定到临时值,这很好 - 它们可以延长寿命。

现在只更改template <typename... Ts> struct zip_tuple : std::tuple<Ts...> { using base = std::tuple<Ts...>; using base::base; template <typename... Us, std::enable_if_t<(std::is_constructible_v<Ts, Us&&> && ...), int> = 0> zip_tuple(std::tuple<Us...>&& rhs) : base(std::move(rhs)) { } template <size_t I> auto& get() & { return std::get<I>(*this); }; template <size_t I> auto& get() const& { return std::get<I>(*this); }; template <size_t I> auto get() && { return std::get<I>(*this); }; template <size_t I> auto get() const&& { return std::get<I>(*this); }; }; namespace std { template <typename... Ts> struct tuple_size<zip_tuple<Ts...>> : tuple_size<tuple<Ts...>> { }; template <size_t I, typename... Ts> struct tuple_element<I, zip_tuple<Ts...>> : tuple_element<I, tuple<remove_reference_t<Ts>...>> { }; } 别名为deref_types而不是zip_tuple,您就可以获得所需的行为。

两个不相关的笔记。

1)您的扣除指南可以简化为:

std::tuple

如果template <typename... Container_types> zip(Container_types&&... containers) -> zip<Container_types...>; 不是左值引用类型,那么它就不是引用类型,而Container_types remove_reference_t<Container_types>

2)gcc对于您尝试构建Container_types的方式有a bug。因此,要使用两者进行编译,首选:

zip<>

你的目的是要经过扣除指南,所以这不应该让你付出任何代价,而是让它在多个编译器上工作。

答案 1 :(得分:1)

您可以使用

轻松地“宣传”​​引用语义
for (auto&& [x, y] : zip(a, b)) {

没有专家会“为它而堕落”,但希望他们明白,即使使用auto [x, y],价值也适用于复合(由于显而易见的原因必须是prvalue),而不是分解的名称,永远不会复制任何东西(除非定制的get使它们成为这样)。