【问题标题】:Concept-restricted range-based for loop of std::list<std::reference_wrapper<T>>std::list<std::reference_wrapper<T>> 的概念受限的基于范围的 for 循环
【发布时间】:2021-10-07 03:17:42
【问题描述】:

我有一些类 Foo 和一个 std::list&lt;std::reference_wrapper&lt;Foo&gt;&gt; 并希望使用基于范围的 for 循环迭代其元素:

#include <list>
#include <functional>
#include <iostream>


class Foo {
public:
    Foo(int a) : a(a) {}
    int a;
};

int main() {
    std::list<Foo> ls = {{1},{2},{3},{4}};
    std::list<std::reference_wrapper<Foo>> refs(ls.begin(), std::next(ls.begin(),2));
    
    for(auto &foo : refs) {
        std::cout << foo.get().a << std::endl;
    }

    for(Foo &foo : refs) {
        std::cout << foo.a << std::endl;
    }

    return 0;
}

注意auto 捕获时附加的get(),因为我们推断出std::reference_wrapper&lt;Foo&gt; 类型,而在第二种情况下foo 已经隐式转换为Foo&amp; 类型,因为我们显式捕获了此类型。

我实际上是在寻找一种方法来捕捉 auto 但隐含地抛弃了 std::reference_wrapper 以便不必一直在 for 正文中打扰 get() 方法,所以我尝试引入一个合适的概念并抓住了这一点,即我尝试过

//this is not legal code

template<typename T>
concept LikeFoo = requires (T t) {
    { t.a };
};

int main() {
    std::list<Foo> ls = {{1},{2},{3},{4}};
    std::list<std::reference_wrapper<Foo>> refs(ls.begin(), std::next(ls.begin(),2));

    for(LikeFoo auto &foo : refs) {
        std::cout << foo.a << std::endl;
    }
    return 0;
}

并希望它会起作用。 clang 但是将foo 的类型推导出为std::reference_wrapper&lt;Foo&gt;,因此实际上下面的代码是正确的:

//this compiles with clang, but not with gcc

template<typename T>
concept LikeFoo = requires (T t) {
    { t.a };
};

int main() {
    std::list<Foo> ls = {{1},{2},{3},{4}};
    std::list<std::reference_wrapper<Foo>> refs(ls.begin(), std::next(ls.begin(),2));

    for(LikeFoo auto &foo : refs) {
        std::cout << foo.get().a << std::endl;
    }
    return 0;
}

然而,gcc 完全拒绝接受基于范围的 for 循环并抱怨 deduced initializer does not satisfy placeholder constraints,因为它试图检查 LikeFoo&lt;std::reference_wrapper&lt;Foo&gt;&gt;,这当然评估为 false,所以使用 gcc 甚至无法捕获 @ 987654342@ 概念受限。出现两个问题:

  • 哪个编译器是正确的? LikeFoo auto&amp; foo : refs 应该有效吗?
  • 有没有办法自动-catch(可能是概念受限的)foo : refs,这样就可以避免在for-loop 主体中写入get()

您可以在Compiler explorer 找到此示例。

【问题讨论】:

  • auto 将始终推断出实际类型。为了获得Foo&amp;,需要进行隐式转换。概念是对允许推导什么类型的约束。它无法应用转化。
  • 是的,我也是这么想的,但是有没有办法在 range-for-loop 的初始化表达式中做到这一点?
  • 您可以将refs 包裹在一个会自动为您提供get() 每个元素的东西中。但是for (auto&amp; foo : unwrap_reference_wrapper(refs)) 看起来更像是for (Foo&amp; : refs) 的更复杂的版本。不过它可以放在通用代码中。
  • 是的,我正在编写大量通用代码,所以这将非常有用。但我不确定我会如何做到这一点,而不必编写一个全新的迭代器类等。

标签: c++ for-loop c++20 concept reference-wrapper


【解决方案1】:

哪些编译器是正确的? LikeFoo auto&amp; foo : refs 应该有效吗?

没有。 refsreference_wrapper&lt;Foo&gt;&amp; 的范围,因此foo 推导出对reference_wrapper&lt;Foo&gt; 的引用——它没有名为a 的成员。受约束的变量声明不会改变演绎的工作方式,它只是有效地表现得像一个额外的static_assert

有没有办法自动捕获(可能是概念受限的)foo : refs,这样就可以避免在 for 循环体中写入 get()

只需写refs?不可以。但是您可以编写一个范围适配器来将您的reference_wrapper&lt;T&gt; 范围转换为T&amp; 的范围。标准库里已经有这样的东西了,transform

for (auto &foo : refs | std::views::transform([](auto r) -> decltype(auto) { return r.get(); })) {

这是一口,所以我们可以让它自己命名适配器:

inline constexpr auto unwrap_ref = std::views::transform(
    []<typename T>(std::reference_wrapper<T> ref) -> T& { return ref; });

然后你可以写:

for (auto &foo : refs | unwrap_ref) { ... }
for (auto &foo : unwrap_ref(refs)) { ... }

不管怎样,foo 在这里推导出为Foo

再做一点工作,您就可以编写一个范围适配器来解开reference_wrapper&lt;T&gt;,但保留任何其他引用类型。

【讨论】:

  • 通过阅读constexpr,我是否正确地解释了get() 已经在运行时应用在那里,所以与for (auto &amp;foo : refs) 相比,运行时没有差异,然后总是得到元素?那太好了!
  • 另外,在我看来clang 目前不会编译这个
【解决方案2】:

这是一个在被取消引用时调用 get 的包装器的最低工作示例。

#include <list>
#include <functional>
#include <iostream>

template <typename T>
struct reference_wrapper_unpacker {
    struct iterator {
        typename T::iterator it;

        iterator& operator++() {
            it++;
            return *this;
        }

        iterator& operator--() {
            it--;
            return *this;
        }

        typename T::value_type::type& operator*() {
            return it->get();
        }

        bool operator!=(const iterator& other) const {
            return it != other.it;
        }
    };
    reference_wrapper_unpacker(T& container) : t(container) {}

    T& t;
    
    iterator begin() const {
        return {t.begin()};
    }

    iterator end() const {
        return {t.end()};
    }
};

class Foo {
public:
    Foo(int a) : a(a) {}
    int a;
};

int main() {
    std::list<Foo> ls = {{1},{2},{3},{4}};
    std::list<std::reference_wrapper<Foo>> refs(ls.begin(), std::next(ls.begin(),2));
    
    for(auto &foo : refs) {
        std::cout << foo.get().a << std::endl;
    }

    for(Foo &foo : refs) {
        std::cout << foo.a << std::endl;
    }

    for(auto &foo : reference_wrapper_unpacker{refs}) {
        std::cout << foo.a << std::endl;
    }

    return 0;
}

要使其在通用代码中可用,您需要 SFINAE 来检测容器是否确实具有 reference_wrapper,如果没有,则返回原始容器。

我将省略那部分,因为它不是原始问题的一部分。

【讨论】:

    猜你喜欢
    • 2020-05-18
    • 2014-01-11
    • 2014-06-28
    • 2011-10-21
    • 1970-01-01
    • 1970-01-01
    • 2021-10-21
    • 1970-01-01
    相关资源
    最近更新 更多