Maximilian Keßler
Maximilian Keßler

Reputation: 515

Concept-restricted range-based for loop of std::list<std::reference_wrapper<T>>

I have some class Foo and a std::list<std::reference_wrapper<Foo>> and would like to iterate over its elements with a range-based for loop:

#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;
}

Notice the additional get() when catching with auto, as we deduce type std::reference_wrapper<Foo>, whereas in the second case foo is already implicitly converted to type Foo& as we explicitly catch with this type.

I was actually looking for a way to catch with auto but implicitly cast away the std::reference_wrapper implicitly in order to not have to bother with the get() method all the time in the for body, so I tried introducing a fitting concept and catching with this, i.e. I tried

//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;
}

and hoped that it would work. clang however deduces the type of foo to std::reference_wrapper<Foo>, so that in fact below code will be correct:

//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;
}

However, gcc completely refuses to accept the range-based for loop and complains deduced initializer does not satisfy placeholder constraints, as it tries to check LikeFoo<std::reference_wrapper<Foo>>, which of course evaluates to false, so with gcc one cannot even catch foo concept-restricted. Two questions arise:

You can find this example at the Compiler explorer.

Upvotes: 2

Views: 479

Answers (2)

Barry
Barry

Reputation: 303347

Which of the compilers is correct? Should LikeFoo auto& foo : refs be valid?

No. refs is a range of reference_wrapper<Foo>&, so foo deduces to a reference to reference_wrapper<Foo> - which does not have a member named a. A constrained variable declaration doesn't change how deduction works, it just effectively behaves like an extra static_assert.

Is there a way to auto-catch (possibly concept-restricted) foo : refs such that one can avoid having to write get() in the for-loop body?

Just by writing refs? No. But you can write a range adaptor to convert your range of reference_wrapper<T> to a range of T&. There is already such a thing in the standard library, transform:

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

That's a mouthful, so we can make it its own named adaptor:

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

And then you can write either:

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

Either way, foo here deduces to be a Foo.

With a little bit more work, you can write a range adaptor that unwraps reference_wrapper<T> but preserves any other reference type.

Upvotes: 2

super
super

Reputation: 12968

Here is a bare minimum working example for a wrapper that calls get when being dereferenced.

#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;
}

To make it usable in generic code you would need to SFINAE to detect if the container actually has a reference_wrapper, and if not, just return the original container.

I'll leave that part out since it was not a part of the original question.

Upvotes: 0

Related Questions