【问题标题】:Structured bindings and tuple of references结构化绑定和引用元组
【发布时间】:2018-04-03 10:59:33
【问题描述】:

我在设计一个简单的 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> - 其中迭代器是压缩容器的迭代器的类型 - 来跟踪其在压缩容器中的位置。当我取消引用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
    //}

}

【问题讨论】:

  • 不是答案,但由于您使用的是 C++17,因此您可以使用折叠表达式而不是 auto _ = {(fn(std::get&lt;Ns&gt;(argument)), 0)...};
  • @jdehesa:我不确定我会达到什么目的?引用元组和reference_wrappers 元组基本上具有相同的行为。其实一开始我也用过,但是搞砸了一些模板推导。
  • 我不明白这个问题。 zip() 绝对应该给你一个 tuple&lt;T&amp;...&gt; 来迭代,没有其他意义 - 如果人们不明白这一点,那就是他们?
  • @Barry:哦,你很好地理解了这个问题。即使我倾向于同意您的回答,我觉得让客户发现结构化绑定会违反直觉地工作并不理想。此外,如果发现这是结构绑定的代表性用例,我也不会感到惊讶,我们应该设计一种变通方法,或者更好地宣传缺点。
  • @papagaga:“我觉得让客户发现结构化绑定会反直觉地工作并不理想。”但这与结构化绑定。你可以很容易地完成for(auto val : zip(...)),而val 仍然包含引用。

标签: c++ c++17


【解决方案1】:

从技术上讲,这与其说是结构化绑定问题,不如说是引用语义类型问题。 auto x = y 看起来确实是在复制然后作用于一个独立的类型,而 tuple&lt;T&amp;...&gt; (以及 reference_wrapper&lt;T&gt;string_viewspan&lt;T&gt; 等类型)绝对不是这种情况。

但是,作为 T.C.在 cmets 中建议,您可以做一些可怕的事情来完成这项工作。请注意,实际上不要这样做。我认为你的实现是正确的。但只是为了完整性。和普遍的兴趣。

首先,结构化绑定的措辞表明get() 的调用方式基于underlying object 的值类别有所不同。如果它是左值引用(即auto&amp;auto const&amp;),则在左值上调用get()。否则,它会在 xvalue 上调用。我们需要利用这一点:

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

做一件事,然后

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

做点别的。其他东西首先需要实际编译。为此,您的 zip_iterator::operator* 需要返回一个左值。要做到,它实际上需要在其中存储一个tuple&lt;T&amp;...&gt;。最简单的方法(在我看来)是存储optional&lt;tuple&lt;T&amp;...&gt;&gt; 并让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;
};

但这仍然给我们带来了想要不同get()s 的问题。为了解决这个问题,我们需要我们自己的tuple 类型——它提供自己的get()s,这样当调用一个左值时它会产生左值,但是当调用一个xvalue 时它会产生纯右值。

我认为是这样的:

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>...>>
    { };
}

在非左值引用的情况下,这意味着我们将一堆rvalue references 绑定到临时对象,这很好——它们的生命周期得到了延长。

现在只需将 deref_types 别名更改为 zip_tuple 而不是 std::tuple,这样您就有了想要的行为。


两个不相关的注释。

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

template <typename... Container_types>
zip(Container_types&&... containers) -> zip<Container_types...>;

如果Container_types 不是左值引用类型,那么它根本就不是引用类型,而remove_reference_t&lt;Container_types&gt; Container_types

2) 关于您尝试构建zip&lt;&gt; 的方式,gcc 有a bug。所以要让它同时编译,更喜欢:

template <typename... Containers>
struct zip {
    zip(Containers... containers) : containers_(std::forward<Containers>(containers)...) { }
};

无论如何,您的预期用途是阅读演绎指南,因此这不应该花费您任何费用来支持它在多个编译器上工作。

【讨论】:

  • 关于第一个“无关笔记”,为什么Container_types不能是右值引用?我的意思是复制一个右值引用将在元组内引用的对象,以避免悬空引用。关于第二个,我无法理解模板推演上下文之外的std::forward。 PS:很棒的答案,谢谢。
  • @papagaga (a) 如果使用右值调用,用于转发引用的模板参数推断为非引用。所以Container_types&amp;&amp; 是一个右值引用,但Container_types 不是一个引用。 (b) 你仍然需要转发,因为containers 可能是左值引用,也可能不是左值引用,只是我们不是在这里推导出左值,而是在更高的层次上。
  • gcc 错误修复会导致不同的行为,具体来说,使用它 gcc 7.4.0 会得到:1、-1 2、-2 3、-3 1、-1 2、-2 3、-3 而铿锵声 6.0.0。得到: 1, -1 2, -2 3, -3 2, -2 3, -3 4, -4 在这两种情况下使用 set_property(TARGET main PROPERTY CXX_STANDARD 17) 其中一个是错误的。哪一个?
  • “其他东西首先需要实际编译。” -> 我很高兴。感谢您提供如此详尽的答案!
【解决方案2】:

您可以简单地“宣传”​​参考语义

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

没有专家会“上当”,但希望他们明白,即使使用auto [x, y],价值也适用于组合(出于显而易见的原因,它必须是纯右值),而不适用于分解的名称,后者永远不会任何东西的副本(除非自定义的get 使它们如此)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-18
    • 2016-08-03
    • 1970-01-01
    • 1970-01-01
    • 2017-02-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多