Reputation: 515
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:
LikeFoo auto& foo : refs
be valid?foo : refs
such that one can avoid having to write get()
in the for
-loop body?You can find this example at the Compiler explorer.
Upvotes: 2
Views: 479
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 writeget()
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
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