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

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

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, но мне кажется, я где-то видел подтверждение, что оно есть. Поправьте меня, пожалуйста, если я ошибаюсь.

4 ответа

Решение

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

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

В настоящее время у меня та же проблема, когда у меня есть вектор объектов. У каждого из них есть член, который указывает, в какой группе они мне нужны. На данный момент лучшее решение, которое я нашел, это:

  1. Сортировка их по идентификатору группы участников
  2. Разделение/разбивка их на части с использованием диапазонов::view::chunk_by Функция, предоставленная chunk_by, затем просто проверит, имеют ли два полученных объекта разные идентификаторы групп.

Обратите внимание, что это решение работает для любого количества подгрупп, а не только для двух, как в примере выше.

Это раздражает. Вы действительно хотите что-то вродеfilterкак представление, которое принимает вызываемый возврат перечисления и возвращаетstd::array<std::vector<T>>.

Или, может быть, у вас есть возможностьtransformк варианту и иметьranges::to_vectorподобная функция, которая разделяет диапазон вариантов (т.е. возвращаетtuple<vector<Ts>...>).

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

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

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

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

Другие вопросы по тегам