#c #for-loop #c 20 #concept #reference-wrapper
Вопрос:
У меня есть некоторый класс Foo
и a std::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 amp;foo : refs) {
std::cout << foo.get().a << std::endl;
}
for(Foo amp;foo : refs) {
std::cout << foo.a << std::endl;
}
return 0;
}
Обратите внимание на дополнительное get()
при ловле с auto
помощью , поскольку мы выводим тип std::reference_wrapper<Foo>
, тогда как во втором случае foo
уже неявно преобразуется в тип Fooamp;
, поскольку мы явно ловим с помощью этого типа.
На самом деле я искал способ поймать с помощью 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 amp;foo : refs) {
std::cout << foo.a << std::endl;
}
return 0;
}
и надеялся, что это сработает. clang
однако выводит тип foo
to 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 amp;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>>
, что, конечно, оценивается как ложное, поэтому с gcc
одним даже невозможно поймать foo
ограничение по концепции. Возникают два вопроса:
- Какой из компиляторов является правильным? Должно
LikeFoo autoamp; foo : refs
быть действительным? - Есть ли способ автоматического перехвата (возможно, с ограничением по концепции)
foo : refs
, чтобы можно было избежать записиget()
вfor
теле цикла?
Вы можете найти этот пример в проводнике компилятора.
Комментарии:
1.
auto
всегда будет выводить фактический тип. Для того, чтобы получитьFooamp;
неявное преобразование, необходимо. Понятие-это ограничение на то, какой тип разрешается выводить. Он не может применять преобразования.2. да, я тоже так подумал, но есть ли способ сделать это в выражении инициализации диапазона для цикла?
3. Вы можете обернуть
refs
во что-то, что автоматическиget()
сделает каждый элемент для вас. Ноfor (autoamp; foo : unwrap_reference_wrapper(refs))
просто выглядит как более запутанная версияfor (Fooamp; : refs)
. Хотя это может иметь свое место в общем коде.4. Да, я пишу тонны общего кода, так что это было бы очень полезно. Но я не уверен, как бы я это сделал, не написав совершенно новый класс итератора и т. Д.
Ответ №1:
Какой из компиляторов является правильным? Должно
LikeFoo autoamp; foo : refs
быть действительным?
No. refs
является диапазоном reference_wrapper<Foo>amp;
, поэтому foo
выводит ссылку на reference_wrapper<Foo>
— у которой нет имени члена a
. Объявление ограниченной переменной не меняет того, как работает дедукция, она просто эффективно ведет себя как дополнительная static_assert
.
Есть ли способ автоматического перехвата (возможно, с ограничением по концепции)
foo : refs
, чтобы можно было избежать записиget()
в теле цикла for?
Просто написав refs
? Нет. Но вы можете написать адаптер диапазона для преобразования вашего диапазона reference_wrapper<T>
в диапазон Tamp;
. В стандартной библиотеке уже есть такая вещь, transform
:
for (auto amp;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) -> Tamp; { return ref; });
И тогда вы можете написать либо:
for (auto amp;foo : refs | unwrap_ref) { ... }
for (auto amp;foo : unwrap_ref(refs)) { ... }
В любом случае, foo
здесь выводится, чтобы быть Foo
А.
Немного поработав, вы можете написать адаптер диапазона, который разворачивает reference_wrapper<T>
, но сохраняет любой другой ссылочный тип.
Комментарии:
1. прочитав
constexpr
, правильно ли я интерпретирую это, чтоget()
уже применяется там во время выполнения, поэтому не будет никакой разницы во времени выполнения по сравнению с тем, чтобы простоfor (auto amp;foo : refs)
и всегда получать элемент? Это было бы здорово!2. кроме того, мне кажется
clang
, что в настоящее время это не будет компилироваться
Ответ №2:
Вот минимальный рабочий пример для оболочки, которая вызывает get
при разыменовании.
#include <list>
#include <functional>
#include <iostream>
template <typename T>
struct reference_wrapper_unpacker {
struct iterator {
typename T::iterator it;
iteratoramp; operator () {
it ;
return *this;
}
iteratoramp; operator--() {
it--;
return *this;
}
typename T::value_type::typeamp; operator*() {
return it->get();
}
bool operator!=(const iteratoramp; other) const {
return it != other.it;
}
};
reference_wrapper_unpacker(Tamp; container) : t(container) {}
Tamp; 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 amp;foo : refs) {
std::cout << foo.get().a << std::endl;
}
for(Foo amp;foo : refs) {
std::cout << foo.a << std::endl;
}
for(auto amp;foo : reference_wrapper_unpacker{refs}) {
std::cout << foo.a << std::endl;
}
return 0;
}
Чтобы сделать его пригодным для использования в общем коде, вам потребуется SFINAE, чтобы определить, действительно ли в контейнере есть reference_wrapper, а если нет, просто верните исходный контейнер.
Я опущу эту часть, так как она не была частью первоначального вопроса.