Библиотека C++ range-v3: "брать" первые 3 совершенных числа работает и останавливается; «взять» первые 4 не останавливается после 4

Насколько я понимаю, операции просмотра библиотеки range-v3 (в настоящее время требуется С++ 17, но чтобы стать официальной частью STL в С++ 20) предоставляют цепные алгоритмы, подобные STL, которые оцениваются лениво. В качестве эксперимента я создал следующий код для оценки первых 4 совершенных чисел:

#include <iostream>
#include <range/v3/all.hpp>

using namespace std;
int main(int argc, char *argv[]) {
    auto perfects = ranges::view::ints(1)
                    | ranges::view::filter([] (int x) {
                        int psum = 0;
                        for (int y = 1; y < x; ++y) {
                            if (x % y == 0) psum += y;
                        }
                        return x == psum;})
                    | ranges::view::take(3);
    std::cout << "PERFECT NUMBERS:" << std::endl;
    for (int z : perfects) {
        std::cout << z << std::endl;
    } 
    std::cout << "DONE." << std::endl;
}

Код начинается с возможного бесконечного диапазона чисел (ranges::view::ints(1)), но поскольку алгоритм просмотра заканчивается на ranges::view::take(3), он должен остановиться после нахождения первых трех чисел, прошедших алгоритм фильтра (алгоритм грубой силы для фильтрации идеальных чисел, намеренно не эффективный). Поскольку первые три совершенных числа --- 6, 28 и 496 --- довольно малы, я ожидаю, что этот код быстро их найдет, выведите «DONE». и прекратить. И это именно то, что происходит:

coliru — 3 совершенных числа работает отлично

Однако, скажем, я хочу напечатать первые 4 совершенных числа, которые все еще довольно малы — 6, 28, 496 и 8128. После вывода 8128 программа не останавливается и в конце концов должна быть завершена; по-видимому, он тщетно пытается вычислить пятое совершенное число, 33550336, которое этот алгоритм грубой силы не может эффективно найти.

coliru – если 4 идеальных числа пытаются получить 5+

Мне это кажется нелогичным. Я бы понял, если бы оба теста провалились (приходя к выводу, что я неправильно понял ленивую оценку алгоритмов представления range-v3), но тот факт, что take(3) завершается успешно и останавливается, в то время как take(4) не кажется мне ошибкой , если я не понимаю вещи.

Я пробовал это с несколькими компиляторами на wandbox, и это кажется постоянным (пробовал clang 6.0.1 и 7.0.0, g++ 8.1.0 и 8.2.0). По крайней мере, на моем локальном компьютере, где я изначально обнаружил проблему, используется версия 0.3.6 range-v3, но я не уверен насчет coliru и wandbox.

ссылка на wandbox


person jwimberley    schedule 16.11.2018    source источник
comment
Всего один комментарий, чтобы помочь людям избежать бесплодных запросов: если вы добавите std::cout ‹‹ Checking ‹‹ x ‹‹ std::endl в тело лямбда-функции, вы увидите вывод кода, например Checking 46838. , так что очевидно, что лямбда-функция оценивается после 4-го совершенного числа (и код не зависает по какой-то другой причине)   -  person jwimberley    schedule 16.11.2018
comment
Еще один совет: поиск чисел, которые не являются совершенными, также приводит к странным результатам. Сохранение оператора cout, изменение результата лямбда на !(x==psum) и получение первых 8124 несовершенных чисел (последнее из которых равно 8127, чтобы компенсировать отсутствие 6, 28 или 426) , лямбда-функция по-прежнему оценивается для 8128 и 8129, прежде чем цикл for остановится. Я предполагаю, что представлению нужен конечный итератор, и поэтому он продолжает оценивать лямбда-функцию, пока не вернет true, чтобы получить конечный итератор.   -  person jwimberley    schedule 16.11.2018
comment
Простой способ подтвердить вышесказанное с помощью гораздо более простой лямбда-функции, которая возвращает true для x ‹= n, false для n ‹ x ‹ n+m и true для x ›= m: ссылка на wandbox. Лямбда-функция явно вызывается до тех пор, пока не вернет true даже после первых n успехов. Теперь возникает вопрос, почему это делается. Это не то, что я ожидал; это может привести к неожиданному поведению (например, если тело лямбда-функции не является чистым и изменяет захваченные объекты], и это может иметь большие последствия для производительности.   -  person jwimberley    schedule 16.11.2018
comment
Я также вижу некоторые комментарии в документации range-v3 о том, что фильтр устарел; но просто хочу подтвердить, что remove_if ведет себя так же.   -  person jwimberley    schedule 16.11.2018
comment
Пожалуйста, разместите всю необходимую информацию по вашему вопросу, не ссылайтесь на внешние ресурсы.   -  person Sakura Kinomoto    schedule 06.12.2018
comment
@SakuraKinomoto Я здесь не согласен. Ссылка на онлайн-компиляторы, такие как coliru, godbolt или wandbox, является обычной практикой для вопросов C++ по SO. Я согласен с тем, что показывать код только на одном из этих сайтов и не включать его в вопрос было бы не очень хорошей практикой (потому что люди, читающие вопрос, не могли получить четкого представления о проблеме, прочитав вопрос), но я не Это не сделано --- код, включенный в вопрос, почти идентичен коду coliru/wandbox, за исключением тривиальных и объясненных модификаций.   -  person jwimberley    schedule 06.12.2018


Ответы (2)


Представление дубля, содержащее n элементов, имеет n + 1 допустимых значений итератора: n, которые соответствуют элементам в диапазоне, и n + 1st итератор за концом. Предполагается, что итерация по представлению Take обязательно формирует каждый из этих итераторов n + 1 — действительно, полезно извлечь базовое значение итератора, адаптированное итератором end представления Take для выполнения дополнительных вычислений.

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

person Casey    schedule 16.11.2018
comment
А, спасибо за объяснение! Для конечного итератора имеет смысл обычно разрешать доступ к базовому итератору, и это требует наблюдаемого поведения. Я рад, что мое злоупотребление функцией может быть сделано примером для уточнения требований к сложности :-) - person jwimberley; 16.11.2018
comment
@jwimberley Пожалуйста. И спасибо вам за то, что заставили меня понять, что мы забыли наложить необходимое требование сложности на функции, используемые с filter_view и transform_view. - person Casey; 17.11.2018
comment
Добро пожаловать! В более общем смысле, не может ли требование сложности для разработчика гарантировать, что для предиката P while (!P(*iter)) { ++iter; } равно O(1)? Ужасное неправильное использование, такое как предикат O(1) x <= 4, будет вести себя по существу так же, как предикат perfect(x) при извлечении 4 или более из целых чисел [1,infinity). Более реалистично, может быть вполне разумно выглядящий предикат, но если записи, передающие предикат, не распределены в контейнере случайным образом, увеличение итератора, следующая запись, передающая предикат, может не быть O (1). - person jwimberley; 17.11.2018

Извинения:

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

Фильтрация до тех пор, пока не будет найден конечный итератор

Я не очень хорошо разбираюсь во внутренностях range-v3, поэтому у меня может быть не совсем правильная терминология. Короче говоря, здесь нет непоследовательного поведения. Когда вызов ranges::view::take следует за вызовом ranges::view::filter (или ranges::view::remove_if), результирующий объект представления должен установить конечный итератор в какой-то момент во время итерации, чтобы выйти из цикла for. Если бы я подумал об этом, я бы предположил, что цикл for, основанный на диапазоне, все еще расширяется до чего-то вроде

for (auto it = std::begin(perfects); it != std::end(perfects); ++it) {
    ...
}

(который, кстати, ведет себя одинаково в моих примерах) и что после того, как он найдет нужное количество элементов, в начале последующего operator++ вызова it должна быть специальная логика, чтобы сделать результат равным std::end(perfects), так что цикл завершается, не выполняя никакой дополнительной работы. Но вместо этого, и это имеет некоторый смысл с точки зрения реализации, конечный итератор фактически соответствует следующему элементу, возвращаемому представлением filter/remove_if. Предикат filter продолжает перебирать ranges::view::ints(1), пока не найдет тот, для которого предикат возвращает true; предположительно это становится конечным итератором, так как он не печатается в цикле for.

Простая демонстрация этого обеспечивается следующим кодом. Здесь есть два настраиваемых целых числа n и m, а функция предиката в filter возвращает true для x <= n, false для n < x < n+m и true для x >= m:

#include <iostream>
#include <range/v3/all.hpp>

using namespace std;
int main(int,char**) {
    int n = 5;
    int m = 3;
    auto perfects = ranges::view::ints(1)
                    | ranges::view::filter([&n,&m] (int x) {
                        std::cout << "Checking " << x << "... ";
                        if (x <= n) {
                            return true;
                        } else if (x <= n + m) {
                            std::cout << std::endl;
                            return false;
                        }
                        return true;})
                    | ranges::view::take(n);
    std::cout << "First " << n << " numbers:" << std::endl;
    for (int z : perfects) {
        std::cout << " take it!" << std::endl;
    }
    std::cout << "DONE." << std::endl;
}

Вы можете запустить этот код для разных значений n и m здесь: wandbox. Вывод по умолчанию выглядит следующим образом:

First 5 numbers:
Checking 1...  take it!
Checking 2...  take it!
Checking 3...  take it!
Checking 4...  take it!
Checking 5...  take it!
Checking 6... 
Checking 7... 
Checking 8... 
Checking 9... DONE.

(Я не переименовывал переменную perfects, очевидно, это уже не набор совершенных чисел). Даже после первых n успехов лямбда-предикат вызывается до тех пор, пока не вернет true. Так как целое число, которое возвращает истину, 9, не печатается, это должно быть std::end(perfects), которое разрывает ранжированный цикл for.

Для меня остается загадкой, почему он это делает. Это не то, что я ожидал; это может привести к неожиданному поведению (например, если тело лямбда-функции не является чистым и изменяет захваченные объекты) и может иметь большие последствия для производительности, как показано в исходном примере, который должен будет выполнить примерно 10 ^ 15 операций по модулю, прежде чем достигнет целое число 33550336.

person jwimberley    schedule 16.11.2018
comment
Не извиняйтесь за то, что придумали свой собственный ответ. Я рад, что вы сделали, и я уверен, что другие будут тоже. - person Pezo; 16.11.2018