Существует ли аналог ranges::views::group_by, который учитывает все элементы, а не только смежные?

В std::ranges C++20 мы можем ожидать получения views::group_by1. Это может быть очень удобно, но во время игры я обнаружил проблему. Из руководства Эрика Ниблера мы можем прочитать, что "< em>По сути, views::group_by группирует непрерывные элементы вместе с бинарным предикатом.". Давайте рассмотрим пример. У меня есть std::vector из int, и я хочу сгруппировать его в два диапазона, представляющих четные и нечетные числа. Мой первоначальный подход состоял в том, чтобы просто сделать:

int main() {
    using namespace ranges;

    std::vector<int> ints = {3, 9, 12, 10, 7, 5, 1, 4, 8};

    for (auto rng : ints | views::group_by(
            [](auto lhs, auto rhs) {
                const bool leftEven = lhs % 2 == 0;
                const bool rightEven = rhs % 2 == 0;

                return (leftEven && rightEven) || (!leftEven && !rightEven);
            })) {
        std::cout << rng << '\n';
    }
}

Но это не сработает. Или, другими словами, это будет работать, но даст неожиданные (для некоторых, я полагаю) результаты для тех, кто знаком с аналогичными операциями в других языках (или даже API). Вывод этой программы:

[3,9]
[12,10]
[7,5,1]
[4,8]

Не все четные и нечетные числа сгруппированы, потому что они не все смежные. 3 и 9 объединены вместе, потому что они оба и непрерывны. Аналогично (за исключением того, что они четные) 12 и 10. Но 7, 5 и 1 создадут отдельную группу - они не будут объединены с 3 и 9, а этого я бы не хотел и не ожидал.

Что мы могли бы, конечно, сделать, так это partition вектор ints упорядочить элементы так, чтобы четы и шансы образовали две группы. Проблема в том, что в диапазонах нет views::partition. Это оставляет мне два варианта, ни один из которых мне особенно не нравится:

1. stdranges::partition перед просмотром вектора:

Вызов:

ranges::partition(ints, [](auto elem) { return elem % 2 == 0; });

непосредственно перед нашим циклом for на основе диапазона, и у нас есть желаемый результат:

[8,4,12,10]
[7,5,1,9,3]

Мне это не нравится, потому что ему не хватает компонуемости — одного из ключевых факторов ranges. Честно говоря, я тоже не хочу разбивать вектор. Я хочу напечатать его элементы в двух группах - четы и шансы.

2. Используйте actions::sort и отсортируйте вектор, используя компаратор чет-нечет:

int main() {
    using namespace ranges;

    std::vector<int> ints = {3, 9, 12, 10, 7, 5, 1, 4, 8};

    auto evens_first = [](auto lhs, auto rhs) { return lhs % 2 == 0 && rhs % 2 != 0; };

    for (auto rng : (ints |= actions::sort(evens_first)) | views::group_by(
            [](auto lhs, auto rhs) {
                const bool leftEven = lhs % 2 == 0;
                const bool rightEven = rhs % 2 == 0;

                return (leftEven && rightEven) || (!leftEven && !rightEven);
            })) {
        std::cout << rng << '\n';
    }
}

Обратите внимание, что круглые скобки вокруг оператора |= обязательны, так как в противном случае оператор составления (|) диапазонов будет оцениваться первым, и мы получим приведенный выше код, печатающий отсортированные элементы вектора, полностью игнорируя группировку (? ??).

Этот подход неплохой, но все же не лучший. Я бы предпочел либо иметь group_by, который мог бы, например, принимать значение и возвращать ключ (подход Java и C# к обработке группировки), либо каким-либо образом учитывать весь диапазон, или, по крайней мере, иметь actions::partition доступным.

Примечание: я вижу смысл в том, что views::grouping_by работает только с непрерывными элементами. Это самый эффективный способ - не нужно ничего сохранять, не нужно возвращаться назад или смотреть дальше. Это нормально, а иногда это лучший инструмент для работы. Но я считаю, что это создает путаницу, будучи нелогичным для людей, которые работали с подобными API в прошлом.

И, наконец, повторю вопрос - есть ли более краткий способ сделать то, что я хочу, на основе предложенных мной примеров и желаемых подходов?


1 Я не могу найти его на cppreference, но Мне кажется, я где-то видел подтверждение того, что он есть. Поправьте меня, пожалуйста, если я ошибаюсь.


person Fureeish    schedule 31.10.2019    source источник
comment
Для справки: я не вижу этого конкретного представления в черновике C++20.   -  person chris    schedule 31.10.2019
comment
@chris то же самое, отсюда и сноска   -  person Fureeish    schedule 31.10.2019
comment
На данный момент, если этого нет в рабочем проекте, я ожидаю, что его не будет и в C++20. cppreference может отставать в основном от того, сколько времени есть у Тристана, но с каждой встречей черновик становится все более и более окончательным.   -  person chris    schedule 31.10.2019
comment
Почему бы не использовать std::views::filter()?   -  person Deduplicator    schedule 31.10.2019
comment
@deduplicator для чего именно?   -  person Fureeish    schedule 31.10.2019
comment
Чтобы получить элементы, соответствующие вашему предикату. И добавление std::not_fn() для тех, кто этого не делает.   -  person Deduplicator    schedule 31.10.2019
comment
Кстати, почему бы и не return !(lhs % 2) == !(rhs % 2);?   -  person Deduplicator    schedule 31.10.2019
comment
@deduplicator Я не хочу получать элементы. Я хочу получить две группы элементов в виде диапазонов. Вы можете объединить два отфильтрованных диапазона, но это ухх. Насчет сказуемого - потому что lhs % 2 == rhs % 2 короче ;›   -  person Fureeish    schedule 31.10.2019
comment
Протестируйте его с элементами -1 и +1. Используя мое предложение или ваше чрезмерное в посте, они находятся в одной группе. Но если взять их % 2, они все равно -1 и +1.   -  person Deduplicator    schedule 31.10.2019
comment
@deduplicator ах, просто предикат. Да, вы правы, ваш подход лучше.   -  person Fureeish    schedule 31.10.2019
comment
Это не в 20.   -  person T.C.    schedule 31.10.2019


Ответы (2)


group_by в этих других языках получает не компаратор для двух элементов, а проекцию элемента на кортеж релевантных значений, на котором работает хеширование и сравнение. Кроме того, они могут свободно выделять дополнительную память для выполнения работы.

Здесь вы не платите за то, чем не пользуетесь, правда, немного снижает ваш комфорт. Вы должны сделать этот шаг явно, если вам это нужно, вместо того, чтобы язык навязывал его вам, даже если это просто бесполезная работа.

person Deduplicator    schedule 31.10.2019

Итак, вам нужен оператор «группировать по» в смысле SQL, верно? Как и действие сортировки, оператор «группировать по» в смысле SQL является автономным оператором, он не может выдать первый элемент вывода, не увидев последний элемент ввода. Его офлайн-природа делает его некомпоновочным по сути.

В настоящее время диапазоны cpp поддерживают это поведение с помощью действия сортировки и группового представления, это один из методов реализации операции «группировать по» в смысле SQL.

Конечно, вы можете разработать другую реализацию с помощью пользовательского действия, точно такого же, как sort (но по-другому), и скомпоновать его с помощью views::group_by.

Например. std::partition — это один метод, или вы можете разработать другой лучший метод разделения на основе хэша, чтобы сгруппировать входной диапазон в диапазон сегментов, элементы в каждом сегменте имеют одно и то же значение хеш-функции (определяемое лямбдой, переданной в метод разделения ).

person Leonhard Gox    schedule 21.04.2021