Концепция-ограниченный диапазон на основе цикла std::list<std::reference_wrapper>

#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, а если нет, просто верните исходный контейнер.

Я опущу эту часть, так как она не была частью первоначального вопроса.