2

我有一些 classFoo和 astd::list<std::reference_wrapper<Foo>>并且想使用基于范围的 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;
}

注意,当我们推断出 typeget()时,在捕获时附加了额外的内容,而在第二种情况下,当我们显式地捕获此类型时,已经隐式转换为类型。autostd::reference_wrapper<Foo>fooFoo&

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

//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但是推导出footo的类型std::reference_wrapper<Foo>,因此实际上下面的代码是正确的:

//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<std::reference_wrapper<Foo>>,这当然评估为 false,因此gcc甚至无法捕获foo概念限制。出现两个问题:

  • 哪个编译器是正确的?应该LikeFoo auto& foo : refs有效吗?
  • 有没有一种方法可以自动捕获(可能是概念受限的)foo : refs,这样就可以避免get()for循环体中写入?

您可以在编译器资源管理器中找到此示例。

4

2 回答 2

2

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

No.refs是 的范围reference_wrapper<Foo>&,因此foo推断出对reference_wrapper<Foo>- 的引用,它没有名为 的成员a。受约束的变量声明不会改变演绎的工作方式,它只是有效地表现得像一个额外的static_assert.

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

就凭写作refs?不,但是您可以编写一个范围适配器来将您的范围转换reference_wrapper<T>T&. 标准库中已经有这样的东西,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<T>但保留任何其他引用类型。

于 2021-08-01T22:24:17.597 回答
0

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,如果没有,则返回原始容器。

我会留下那部分,因为它不是原始问题的一部分。

于 2021-08-01T19:21:34.847 回答