From 02416eb77a6f2a5b67669a232c9855c6952574ca Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 14:35:44 +0200 Subject: [PATCH 01/20] First draft of lazy operations --- include/vector.h | 208 +++++++++++++++++++++++++++++++++++++++++++ tests/vector_test.cc | 66 ++++++++++++++ 2 files changed, 274 insertions(+) diff --git a/include/vector.h b/include/vector.h index be3da65..9257d5f 100644 --- a/include/vector.h +++ b/include/vector.h @@ -23,9 +23,12 @@ #pragma once #include #include +#include #include #include #include +#include +#include #include "index_range.h" #include "optional.h" #ifdef PARALLEL_ALGORITHM_AVAILABLE @@ -36,6 +39,193 @@ namespace fcpp { template class set; + template + class vector; + + // A lightweight wrapper representing a deferred vector pipeline, enabling fluent and functional + // programming while avoiding intermediate vector materialization. + // + // Member functions are non-mutating and keep extending the pipeline. Terminal functions such as + // `get` and `reduce` execute the stored operations. + template + class lazy_vector + { + public: + lazy_vector() + : m_operation([](const std::function&) {}) + , m_capacity_hint(0) + { + } + + // Creates a lazy vector by copying the provided std::vector as an owned source. + explicit lazy_vector(const std::vector& vector) + : m_capacity_hint(vector.size()) + { + auto source = std::make_shared>(vector); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy vector by moving the provided std::vector as an owned source. + explicit lazy_vector(std::vector&& vector) + : m_capacity_hint(vector.size()) + { + auto source = std::make_shared>(std::move(vector)); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy vector by referring to an existing std::vector source. + // The referenced vector must outlive this lazy vector. + explicit lazy_vector(const std::vector* vector) + : m_capacity_hint(vector->size()) + { + m_operation = [vector](const std::function& consumer) { + std::for_each(vector->begin(), vector->end(), consumer); + }; + } + + // Creates a lazy vector by directly providing the deferred operation. + // This constructor is mostly useful for composing lazy_vector instances. + lazy_vector(std::function&)> operation, size_t capacity_hint) + : m_operation(std::move(operation)) + , m_capacity_hint(capacity_hint) + { + } + + // Performs the functional `map` algorithm lazily. The transform is not applied until + // a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector input_vector({ 1, 3, -5 }); + // const auto output_vector = input_vector + // .lazy() + // .map([](const auto& element) { + // return std::to_string(element); + // }) + // .get(); + // + // outcome: + // output_vector -> fcpp::vector({ "1", "3", "-5" }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector map(Transform&& transform) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + typename std::decay::type transform_copy(std::forward(transform)); + return lazy_vector( + [previous, transform_copy](const std::function& consumer) mutable { + previous([&consumer, &transform_copy](const T& element) { + consumer(transform_copy(element)); + }); + }, + capacity_hint); + } + + // Performs the functional `map` algorithm lazily. + // See also `map` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector mapped(Transform&& transform) const + { + return map(std::forward(transform)); + } + + // Performs the functional `filter` algorithm lazily, in which all elements which match + // the given predicate are kept. The predicate is not applied until a terminal operation, + // such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto filtered_numbers = numbers + // .lazy() + // .filter([](const auto& element) { + // return element >= 1.5; + // }) + // .get(); + // + // outcome: + // filtered_numbers -> fcpp::vector({ 3, 2, 9 }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector filter(Filter&& predicate_to_keep) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + typename std::decay::type predicate_copy(std::forward(predicate_to_keep)); + return lazy_vector( + [previous, predicate_copy](const std::function& consumer) mutable { + previous([&consumer, &predicate_copy](const T& element) { + if (predicate_copy(element)) { + consumer(element); + } + }); + }, + capacity_hint); + } + + // Performs the functional `filter` algorithm lazily. + // See also `filter` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector filtered(Filter&& predicate_to_keep) const + { + return filter(std::forward(predicate_to_keep)); + } + + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of + // accumulating all the values in this lazy vector to an initial value. + // + // example: + // const fcpp::vector numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto sum = numbers + // .lazy() + // .filter([](const auto& element) { + // return element > 0; + // }) + // .reduce(0, [](const int& partial_sum, const int& number) { + // return partial_sum + number; + // }); + // + // outcome: + // sum -> 15 +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + U reduce(const U& initial, Reduce&& reduction) const + { + auto result = initial; + m_operation([&result, &reduction](const T& element) { + result = reduction(result, element); + }); + return result; + } + + // Materializes this lazy vector to a functional vector, executing all stored operations. + [[nodiscard]] vector get() const; + + private: + std::function&)> m_operation; + size_t m_capacity_hint; + }; + // A lightweight wrapper around std::vector, enabling fluent and functional // programming on the vector itself, rather than using the more procedural style // of the standard library algorithms. @@ -1429,6 +1619,13 @@ namespace fcpp { return *this; } + // Starts a lazy pipeline. The returned lazy vector defers following map/filter + // transformations until a terminal operation, such as get() or reduce(), is called. + [[nodiscard]] lazy_vector lazy() const + { + return lazy_vector(&m_vector); + } + // Returns the begin iterator, useful for other standard library algorithms [[nodiscard]] typename std::vector::iterator begin() { @@ -1685,4 +1882,15 @@ namespace fcpp { assert(index <= size()); } }; + + template + [[nodiscard]] vector lazy_vector::get() const + { + std::vector materialized; + materialized.reserve(m_capacity_hint); + m_operation([&materialized](const T& element) { + materialized.push_back(element); + }); + return vector(std::move(materialized)); + } } diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 7e44f94..12d1782 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -1375,4 +1375,70 @@ TEST(VectorTest, DistinctCustomType) EXPECT_EQ(expected, unique_persons); } +TEST(VectorTest, LazyMapFilterGet) +{ + const vector vector_under_test({1, 2, 3, 4}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto lazy_vector = vector_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 2; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 4; + }); + + EXPECT_EQ(0, map_call_count); + EXPECT_EQ(0, filter_call_count); + + const auto materialized_vector = lazy_vector.get(); + EXPECT_EQ(vector({6, 8}), materialized_vector); + EXPECT_EQ(vector({1, 2, 3, 4}), vector_under_test); + EXPECT_EQ(4, map_call_count); + EXPECT_EQ(4, filter_call_count); +} + +TEST(VectorTest, LazyFiltered) +{ + const vector vector_under_test({1, 2, 3, 4}); + const auto filtered_vector = vector_under_test + .lazy() + .filtered([](const int& value) { + return value % 2 == 0; + }) + .get(); + + EXPECT_EQ(vector({2, 4}), filtered_vector); + EXPECT_EQ(vector({1, 2, 3, 4}), vector_under_test); +} + +TEST(VectorTest, LazyReduce) +{ + const vector vector_under_test({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto result = vector_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 3; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 5; + }) + .reduce(0, [](const int& partial_sum, const int& value) { + return partial_sum + value; + }); + + EXPECT_EQ(162, result); + EXPECT_EQ(10, map_call_count); + EXPECT_EQ(10, filter_call_count); +} + #pragma warning( pop ) From 939ce86e4cd88e888969ead1eb7a8d6ca79790db Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 15:00:47 +0200 Subject: [PATCH 02/20] Sorting lazy vectors --- include/vector.h | 77 ++++++++++++++++++++++++++++++++++++++++ tests/vector_test.cc | 84 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/include/vector.h b/include/vector.h index 9257d5f..204ce07 100644 --- a/include/vector.h +++ b/include/vector.h @@ -188,6 +188,83 @@ namespace fcpp { return filter(std::forward(predicate_to_keep)); } + // Sorts the lazy vector. The comparison predicate takes two elements + // `v1` and `v2` and returns true if the first element `v1` should appear before `v2`. + // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 3, 1, 9, -4 }); + // const auto sorted_numbers = numbers + // .lazy() + // .sort([](const auto& number1, const auto& number2) { + // return number1 < number2; + // }) + // .get(); + // + // outcome: + // sorted_numbers -> fcpp::vector({ -4, 1, 3, 9 }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector sort(Sortable&& comparison_predicate) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + typename std::decay::type comparison_copy(std::forward(comparison_predicate)); + return lazy_vector( + [previous, capacity_hint, comparison_copy](const std::function& consumer) mutable { + std::vector sorted_vector; + sorted_vector.reserve(capacity_hint); + previous([&sorted_vector](const T& element) { + sorted_vector.push_back(element); + }); + + std::sort(sorted_vector.begin(), + sorted_vector.end(), + comparison_copy); + std::for_each(sorted_vector.begin(), + sorted_vector.end(), + consumer); + }, + capacity_hint); + } + + // Sorts the lazy vector in ascending order, when its elements support comparison by std::less [<]. + // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 3, 1, 9, -4 }); + // const auto sorted_numbers = numbers + // .lazy() + // .sort_ascending() + // .get(); + // + // outcome: + // sorted_numbers -> fcpp::vector({ -4, 1, 3, 9 }) + [[nodiscard]] lazy_vector sort_ascending() const + { + return sort(std::less()); + } + + // Sorts the lazy vector in descending order, when its elements support comparison by std::greater [>]. + // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 3, 1, 9, -4 }); + // const auto sorted_numbers = numbers + // .lazy() + // .sort_descending() + // .get(); + // + // outcome: + // sorted_numbers -> fcpp::vector({ 9, 3, 1, -4 }) + [[nodiscard]] lazy_vector sort_descending() const + { + return sort(std::greater()); + } + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of // accumulating all the values in this lazy vector to an initial value. // diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 12d1782..3889478 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -1441,4 +1441,88 @@ TEST(VectorTest, LazyReduce) EXPECT_EQ(10, filter_call_count); } +TEST(VectorTest, LazySort) +{ + const vector vector_under_test({ + person(45, "Jake"), person(34, "Bob"), person(52, "Manfred"), person(8, "Alice") + }); + + const auto sorted_vector = vector_under_test + .lazy() + .sort([](const person& person1, const person& person2) { + return person1.name < person2.name; + }) + .get(); + + EXPECT_EQ(4, vector_under_test.size()); + EXPECT_EQ("Jake", vector_under_test[0].name); + EXPECT_EQ("Bob", vector_under_test[1].name); + EXPECT_EQ("Manfred", vector_under_test[2].name); + EXPECT_EQ("Alice", vector_under_test[3].name); + + EXPECT_EQ(4, sorted_vector.size()); + EXPECT_EQ("Alice", sorted_vector[0].name); + EXPECT_EQ(8, sorted_vector[0].age); + EXPECT_EQ("Bob", sorted_vector[1].name); + EXPECT_EQ(34, sorted_vector[1].age); + EXPECT_EQ("Jake", sorted_vector[2].name); + EXPECT_EQ(45, sorted_vector[2].age); + EXPECT_EQ("Manfred", sorted_vector[3].name); + EXPECT_EQ(52, sorted_vector[3].age); +} + +TEST(VectorTest, LazySortAscending) +{ + const vector vector_under_test({3, 1, 9, -4}); + + const auto sorted_vector = vector_under_test + .lazy() + .sort_ascending() + .get(); + + EXPECT_EQ(vector({3, 1, 9, -4}), vector_under_test); + EXPECT_EQ(vector({-4, 1, 3, 9}), sorted_vector); +} + +TEST(VectorTest, LazySortDescending) +{ + const vector vector_under_test({3, 1, 9, -4}); + + const auto sorted_vector = vector_under_test + .lazy() + .sort_descending() + .get(); + + EXPECT_EQ(vector({3, 1, 9, -4}), vector_under_test); + EXPECT_EQ(vector({9, 3, 1, -4}), sorted_vector); +} + +TEST(VectorTest, LazyFilterSortMap) +{ + const vector vector_under_test({5, 1, 4, 2, 3}); + int filter_call_count = 0; + int map_call_count = 0; + + const auto lazy_vector = vector_under_test + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 2; + }) + .sort_ascending() + .map([&map_call_count](const int& value) { + ++map_call_count; + return std::to_string(value); + }); + + EXPECT_EQ(0, filter_call_count); + EXPECT_EQ(0, map_call_count); + + const auto materialized_vector = lazy_vector.get(); + + EXPECT_EQ(vector({"3", "4", "5"}), materialized_vector); + EXPECT_EQ(5, filter_call_count); + EXPECT_EQ(3, map_call_count); +} + #pragma warning( pop ) From d49d391dedf3180c8c7a3aa6d71b6eca63351382 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 15:09:34 +0200 Subject: [PATCH 03/20] Update README.md --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index 8ec973d..c0213e8 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,65 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c return partial_sum + p.age; }); ``` + +### lazy vectors +Lazy vectors are useful when chaining multiple operations over a large vector. A regular `map().filter().reduce()` style chain creates intermediate vectors and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is the important exception: it cannot be streamed element by element, so lazy `sort`, `sort_ascending`, and `sort_descending` first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain. + +```c++ +#include "vector.h" // instead of + +const fcpp::vector numbers({5, 1, 4, 2, 3}); + +const auto processed_numbers = numbers + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const int& number) { + return number > 2; + }) + + // sorting is also deferred, but it needs to materialize the filtered + // values internally when the terminal operation is called + .sort_ascending() + + // this transform is not evaluated yet + .map([](const int& number) { + return std::to_string(number); + }) + + // terminal operation: all stored operations are executed here + .get(); + +// processed_numbers -> fcpp::vector({ "3", "4", "5" }) +// numbers -> fcpp::vector({ 5, 1, 4, 2, 3 }) +``` + +Here is another example without sorting, thus all operations are materialized in the end. + +```c++ +const auto total = numbers + // start a lazy pipeline from this point on + .lazy() + + // this transform is not evaluated yet + .map([](const int& number) { + return number * 3; + }) + + // this predicate is not evaluated yet + .filter([](const int& number) { + return number > 5; + }) + + // terminal operation: all stored operations are executed here + .reduce(0, [](const int& partial_sum, const int& number) { + return partial_sum + number; + }); + +// total -> 42 +``` + ### index search ```c++ #include "vector.h" // instead of From 2de8a9082326c75d2f51a137485e694f13e5eb0e Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 20:34:19 +0200 Subject: [PATCH 04/20] Added lazy zip --- README.md | 30 +++++++++++++++- include/vector.h | 80 +++++++++++++++++++++++++++++++++++++++++ tests/vector_test.cc | 84 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0213e8..e089ac8 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c ``` ### lazy vectors -Lazy vectors are useful when chaining multiple operations over a large vector. A regular `map().filter().reduce()` style chain creates intermediate vectors and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is the important exception: it cannot be streamed element by element, so lazy `sort`, `sort_ascending`, and `sort_descending` first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain. +Lazy vectors are useful when chaining multiple operations over a large vector. A regular `map().filter().reduce()` style chain creates intermediate vectors and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is an important exception: it cannot be streamed element by element, so lazy `sort`, `sort_ascending`, and `sort_descending` first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain. ```c++ #include "vector.h" // instead of @@ -194,6 +194,34 @@ const auto total = numbers // total -> 42 ``` +Lazy zip can combine a lazy vector with an `fcpp::vector`, a `std::vector`, or another `fcpp::lazy_vector` and also waits until a terminal operation is called, and only then checks that both sides have equal sizes. When zipping with another lazy vector, the right-hand lazy vector is materialized internally at that point, so its values can be paired by index. + +```c++ +const fcpp::vector ages({32, 45, 37}); +const fcpp::vector names({"Jake", "Anna", "Kate"}); + +const auto employees = ages + // start a lazy pipeline from this point on + .lazy() + + // zip is not evaluated yet + .zip(names) + + // this transform is not evaluated yet + .map([](const std::pair& pair) { + return person(pair.first, pair.second); + }) + + // terminal operation: zip size validation and all stored operations run here + .get(); + +// employees -> fcpp::vector({ +// person(32, "Jake"), +// person(45, "Anna"), +// person(37, "Kate"), +// }) +``` + ### index search ```c++ #include "vector.h" // instead of diff --git a/include/vector.h b/include/vector.h index 204ce07..5032248 100644 --- a/include/vector.h +++ b/include/vector.h @@ -188,6 +188,86 @@ namespace fcpp { return filter(std::forward(predicate_to_keep)); } + // Performs the functional `zip` algorithm lazily, in which every element of the resulting + // lazy vector is a tuple of this instance's element (first) and the second vector's element + // (second) at the same index. The sizes of the two vectors must be equal. + // + // example: + // const fcpp::vector ages_vector({ 32, 25, 53 }); + // const fcpp::vector names_vector({ "Jake", "Mary", "John" }); + // const auto zipped_vector = ages_vector + // .lazy() + // .zip(names_vector) + // .get(); + // + // outcome: + // zipped_vector -> fcpp::vector>({ + // (32, "Jake"), + // (25, "Mary"), + // (53, "John"), + // }) + template + [[nodiscard]] lazy_vector> zip(const vector& vector) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + return lazy_vector>( + [previous, &vector](const std::function&)>& consumer) { + size_t index = 0; + previous([&vector, &consumer, &index](const T& element) { + assert(index < vector.size()); + consumer({element, vector[index]}); + ++index; + }); + assert(index == vector.size()); + }, + capacity_hint); + } + + // Performs the functional `zip` algorithm lazily, in which every element of the resulting + // lazy vector is a tuple of this instance's element (first) and the std::vector's element + // (second) at the same index. The sizes of the two vectors must be equal. + template + [[nodiscard]] lazy_vector> zip(const std::vector& vector) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + return lazy_vector>( + [previous, &vector](const std::function&)>& consumer) { + size_t index = 0; + previous([&vector, &consumer, &index](const T& element) { + assert(index < vector.size()); + consumer({element, vector[index]}); + ++index; + }); + assert(index == vector.size()); + }, + capacity_hint); + } + + // Performs the functional `zip` algorithm lazily, in which every element of the resulting + // lazy vector is a tuple of this instance's element (first) and the second lazy vector's + // element (second) at the same index. The sizes of the two lazy vectors must be equal. + // The right-hand lazy vector is materialized internally when a terminal operation is called. + template + [[nodiscard]] lazy_vector> zip(const lazy_vector& vector) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + return lazy_vector>( + [previous, vector](const std::function&)>& consumer) { + const auto materialized_vector = vector.get(); + size_t index = 0; + previous([&materialized_vector, &consumer, &index](const T& element) { + assert(index < materialized_vector.size()); + consumer({element, materialized_vector[index]}); + ++index; + }); + assert(index == materialized_vector.size()); + }, + capacity_hint); + } + // Sorts the lazy vector. The comparison predicate takes two elements // `v1` and `v2` and returns true if the first element `v1` should appear before `v2`. // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 3889478..2ac49aa 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -1525,4 +1525,88 @@ TEST(VectorTest, LazyFilterSortMap) EXPECT_EQ(3, map_call_count); } +TEST(VectorTest, LazyZipWithFunctionalVector) +{ + const vector vector_under_test({1, 2, 3, 4}); + const vector names({"three", "four"}); + int filter_call_count = 0; + + const auto lazy_vector = vector_under_test + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 2; + }) + .zip(names); + + EXPECT_EQ(0, filter_call_count); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(2, zipped_vector.size()); + EXPECT_EQ(3, zipped_vector[0].first); + EXPECT_EQ("three", zipped_vector[0].second); + EXPECT_EQ(4, zipped_vector[1].first); + EXPECT_EQ("four", zipped_vector[1].second); + EXPECT_EQ(4, filter_call_count); +} + +TEST(VectorTest, LazyZipWithStdVector) +{ + const vector vector_under_test({1, 2, 3}); + const std::vector names({"one", "two", "three"}); + + const auto zipped_vector = vector_under_test + .lazy() + .zip(names) + .get(); + + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(1, zipped_vector[0].first); + EXPECT_EQ("one", zipped_vector[0].second); + EXPECT_EQ(2, zipped_vector[1].first); + EXPECT_EQ("two", zipped_vector[1].second); + EXPECT_EQ(3, zipped_vector[2].first); + EXPECT_EQ("three", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithLazyVector) +{ + const vector ages({32, 45, 37}); + const vector names({"Jake", "Anna", "Kate"}); + int map_call_count = 0; + + const auto lazy_names = names + .lazy() + .map([&map_call_count](const std::string& name) { + ++map_call_count; + return name + "!"; + }); + + const auto lazy_vector = ages + .lazy() + .zip(lazy_names); + + EXPECT_EQ(0, map_call_count); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(3, map_call_count); + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(32, zipped_vector[0].first); + EXPECT_EQ("Jake!", zipped_vector[0].second); + EXPECT_EQ(45, zipped_vector[1].first); + EXPECT_EQ("Anna!", zipped_vector[1].second); + EXPECT_EQ(37, zipped_vector[2].first); + EXPECT_EQ("Kate!", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithDifferentSizesThrows) +{ + const vector vector_under_test({1, 2}); + const std::vector names({"one"}); + + EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); +} + #pragma warning( pop ) From 7dbd369d8b273f891268f52e1cc0eb946a2ecbac Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 20:36:05 +0200 Subject: [PATCH 05/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e089ac8..a2990c7 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c }); ``` -### lazy vectors +### Lazy operations Lazy vectors are useful when chaining multiple operations over a large vector. A regular `map().filter().reduce()` style chain creates intermediate vectors and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is an important exception: it cannot be streamed element by element, so lazy `sort`, `sort_ascending`, and `sort_descending` first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain. ```c++ From c8bfa440d980115a5b607751713a4afec9026834 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 20:59:02 +0200 Subject: [PATCH 06/20] added lazy_set --- README.md | 59 +++++++++++ include/set.h | 265 ++++++++++++++++++++++++++++++++++++++++++++++ tests/set_test.cc | 181 +++++++++++++++++++++++++++++++ 3 files changed, 505 insertions(+) diff --git a/README.md b/README.md index a2990c7..31bbca1 100644 --- a/README.md +++ b/README.md @@ -454,6 +454,65 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c }); ``` +### Lazy operations +Lazy sets are useful when chaining operations over a large set and only needing the final materialized set or a reduced value. A regular `map().filter().reduce()` chain creates intermediate sets and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process keys in one pass. Unlike vectors, sets are already ordered by their comparator, so lazy sets focus on the operations that make sense for set data: `map`, `filter`, `zip`, and `reduce`. + +```c++ +#include "set.h" // instead of + +const fcpp::set numbers({1, 2, 3, 4, 5}); + +const auto total = numbers + // start a lazy pipeline from this point on + .lazy() + + // this transform is not evaluated yet + .map([](const int& number) { + return number * 3; + }) + + // this predicate is not evaluated yet + .filter([](const int& number) { + return number > 5; + }) + + // terminal operation: all stored operations are executed here + .reduce(0, [](const int& partial_sum, const int& number) { + return partial_sum + number; + }); + +// total -> 42 +``` + +Lazy set zip can combine a lazy set with an `fcpp::set`, a `std::set`, an `fcpp::vector`, a `std::vector`, or another `fcpp::lazy_set`. Size validation is deferred until a terminal operation is called. When zipping with a vector, duplicate vector values are removed before zipping, just like the eager set zip operation. When zipping with another lazy set, the right-hand lazy set is materialized internally at that point, so its keys can be paired in set order. + +```c++ +const fcpp::set ages({25, 45, 30, 63}); +const fcpp::set names({"Jake", "Bob", "Michael", "Philipp"}); + +const auto employees = ages + // start a lazy pipeline from this point on + .lazy() + + // zip is not evaluated yet + .zip(names) + + // this transform is not evaluated yet + .map([](const std::pair& pair) { + return person(pair.first, pair.second); + }) + + // terminal operation: zip size validation and all stored operations run here + .get(); + +// employees -> fcpp::set({ +// person(25, "Bob"), +// person(30, "Jake"), +// person(45, "Michael"), +// person(63, "Philipp"), +// }) +``` + ### all_of, any_of, none_of ```c++ #include "set.h" // instead of diff --git a/include/set.h b/include/set.h index 37d8025..e1674a3 100644 --- a/include/set.h +++ b/include/set.h @@ -22,13 +22,261 @@ #pragma once #include +#include +#include +#include #include +#include +#include +#include #include "optional.h" namespace fcpp { template class vector; + template + class set; + + // A lightweight wrapper representing a deferred set pipeline, enabling fluent and functional + // programming while avoiding intermediate set materialization. + // + // Member functions are non-mutating and keep extending the pipeline. Terminal functions such as + // `get` and `reduce` execute the stored operations. + template > + class lazy_set + { + public: + lazy_set() + : m_operation([](const std::function&) {}) + { + } + + // Creates a lazy set by copying the provided std::set as an owned source. + explicit lazy_set(const std::set& set) + { + auto source = std::make_shared>(set); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy set by moving the provided std::set as an owned source. + explicit lazy_set(std::set&& set) + { + auto source = std::make_shared>(std::move(set)); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy set by referring to an existing std::set source. + // The referenced set must outlive this lazy set. + explicit lazy_set(const std::set* set) + { + m_operation = [set](const std::function& consumer) { + std::for_each(set->begin(), set->end(), consumer); + }; + } + + // Creates a lazy set by directly providing the deferred operation. + // This constructor is mostly useful for composing lazy_set instances. + explicit lazy_set(std::function&)> operation) + : m_operation(std::move(operation)) + { + } + + // Performs the functional `map` algorithm lazily. The transform is not applied until + // a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set input_set({ 1, 3, -5 }); + // const auto output_set = input_set + // .lazy() + // .map([](const int& element) { + // return std::to_string(element); + // }) + // .get(); + // + // outcome: + // output_set -> fcpp::set({ "-5", "1", "3" }) +#ifdef CPP17_AVAILABLE + template , typename Transform, typename = std::enable_if_t< + std::is_invocable_r_v>> +#else + template , typename Transform> +#endif + [[nodiscard]] lazy_set map(Transform&& transform) const + { + const auto previous = m_operation; + typename std::decay::type transform_copy(std::forward(transform)); + return lazy_set( + [previous, transform_copy](const std::function& consumer) mutable { + previous([&consumer, &transform_copy](const TKey& key) { + consumer(transform_copy(key)); + }); + }); + } + + // Performs the functional `filter` algorithm lazily, in which all keys which match + // the given predicate are kept. The predicate is not applied until a terminal operation, + // such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto filtered_numbers = numbers + // .lazy() + // .filter([](const int& element) { + // return element >= 1.5; + // }) + // .get(); + // + // outcome: + // filtered_numbers -> fcpp::set({ 2, 3, 9 }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_set filter(Filter&& predicate_to_keep) const + { + const auto previous = m_operation; + typename std::decay::type predicate_copy(std::forward(predicate_to_keep)); + return lazy_set( + [previous, predicate_copy](const std::function& consumer) mutable { + previous([&consumer, &predicate_copy](const TKey& key) { + if (predicate_copy(key)) { + consumer(key); + } + }); + }); + } + + // Performs the functional `filter` algorithm lazily. + // See also `filter` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_set filtered(Filter&& predicate_to_keep) const + { + return filter(std::forward(predicate_to_keep)); + } + + // Performs the functional `zip` algorithm lazily, in which every key of the resulting + // lazy set is a tuple of this instance's key (first) and the second set's key (second). + // The sizes of the two sets must be equal. + template + [[nodiscard]] lazy_set> zip(const set& set) const + { + const auto previous = m_operation; + return lazy_set>( + [previous, &set](const std::function&)>& consumer) { + size_t index = 0; + previous([&set, &consumer, &index](const TKey& key) { + assert(index < set.size()); + consumer({key, set[index]}); + ++index; + }); + assert(index == set.size()); + }); + } + + // Performs the functional `zip` algorithm lazily. + // The sizes of the two sets must be equal. + template + [[nodiscard]] lazy_set> zip(const std::set& set) const + { + const auto previous = m_operation; + return lazy_set>( + [previous, &set](const std::function&)>& consumer) { + auto it = set.begin(); + previous([&set, &it, &consumer](const TKey& key) { + assert(it != set.end()); + consumer({key, *it}); + ++it; + }); + assert(it == set.end()); + }); + } + + // Performs the functional `zip` algorithm lazily where duplicates are removed before zipping. + // The input vector must contain the same number of distinct values as the set size. + template + [[nodiscard]] lazy_set> zip(const vector& vector) const + { + std::set distinct_values(vector.begin(), vector.end()); + return zip(lazy_set(std::move(distinct_values))); + } + + // Performs the functional `zip` algorithm lazily where duplicates are removed before zipping. + // The input vector must contain the same number of distinct values as the set size. + template + [[nodiscard]] lazy_set> zip(const std::vector& vector) const + { + std::set distinct_values(vector.begin(), vector.end()); + return zip(lazy_set(std::move(distinct_values))); + } + + // Performs the functional `zip` algorithm lazily, in which every key of the resulting + // lazy set is a tuple of this instance's key (first) and the second lazy set's key (second). + // The sizes of the two lazy sets must be equal. + // The right-hand lazy set is materialized internally when a terminal operation is called. + template + [[nodiscard]] lazy_set> zip(const lazy_set& set) const + { + const auto previous = m_operation; + return lazy_set>( + [previous, set](const std::function&)>& consumer) { + const auto materialized_set = set.get(); + size_t index = 0; + previous([&materialized_set, &consumer, &index](const TKey& key) { + assert(index < materialized_set.size()); + consumer({key, materialized_set[index]}); + ++index; + }); + assert(index == materialized_set.size()); + }); + } + + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of + // accumulating all the values in this lazy set to an initial value. + // + // example: + // const fcpp::set numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto sum = numbers + // .lazy() + // .filter([](const int& element) { + // return element > 0; + // }) + // .reduce(0, [](const int& partial_sum, const int& number) { + // return partial_sum + number; + // }); + // + // outcome: + // sum -> 15 +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + U reduce(const U& initial, Reduce&& reduction) const + { + auto result = initial; + m_operation([&result, &reduction](const TKey& key) { + result = reduction(result, key); + }); + return result; + } + + // Materializes this lazy set to a functional set, executing all stored operations. + [[nodiscard]] set get() const; + + private: + std::function&)> m_operation; + }; + // A lightweight wrapper around std::set, enabling fluent and functional // programming on the set itself, rather than using the more procedural style // of the standard library algorithms. @@ -612,6 +860,13 @@ namespace fcpp { return m_set.size(); } + // Starts a lazy pipeline. The returned lazy set defers following map/filter/zip + // transformations until a terminal operation, such as get() or reduce(), is called. + [[nodiscard]] lazy_set lazy() const + { + return lazy_set(&m_set); + } + // Returns the begin iterator, useful for other standard library algorithms [[nodiscard]] typename std::set::iterator begin() { @@ -739,4 +994,14 @@ namespace fcpp { return set>(combined_set); } }; + + template + [[nodiscard]] set lazy_set::get() const + { + std::set materialized; + m_operation([&materialized](const TKey& key) { + materialized.insert(key); + }); + return set(std::move(materialized)); + } } diff --git a/tests/set_test.cc b/tests/set_test.cc index 48fe52e..2deceb6 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -568,3 +568,184 @@ TEST(SetTest, EqualityOperatorCustomType) EXPECT_TRUE(set1 == set2); EXPECT_FALSE(set1 != set2); } + +TEST(SetTest, LazyMapFilterGet) +{ + const set set_under_test({1, 2, 3, 4}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto lazy_set = set_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 2; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 4; + }); + + EXPECT_EQ(0, map_call_count); + EXPECT_EQ(0, filter_call_count); + + const auto materialized_set = lazy_set.get(); + EXPECT_EQ(set({6, 8}), materialized_set); + EXPECT_EQ(set({1, 2, 3, 4}), set_under_test); + EXPECT_EQ(4, map_call_count); + EXPECT_EQ(4, filter_call_count); +} + +TEST(SetTest, LazyFiltered) +{ + const set set_under_test({1, 2, 3, 4}); + const auto filtered_set = set_under_test + .lazy() + .filtered([](const int& value) { + return value % 2 == 0; + }) + .get(); + + EXPECT_EQ(set({2, 4}), filtered_set); + EXPECT_EQ(set({1, 2, 3, 4}), set_under_test); +} + +TEST(SetTest, LazyReduce) +{ + const set set_under_test({1, 2, 3, 4, 5}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto result = set_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 3; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 5; + }) + .reduce(0, [](const int& partial_sum, const int& value) { + return partial_sum + value; + }); + + EXPECT_EQ(42, result); + EXPECT_EQ(5, map_call_count); + EXPECT_EQ(5, filter_call_count); +} + +TEST(SetTest, LazyZipWithFunctionalSet) +{ + const set ages({25, 45, 30, 63}); + const set persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithStdSet) +{ + const set ages({25, 45, 30, 63}); + const std::set persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithFunctionalVector) +{ + const set ages({25, 45, 30, 63}); + const vector persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithStdVector) +{ + const set ages({25, 45, 30, 63}); + const std::vector persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithLazySet) +{ + const set ages({25, 45, 30, 63}); + const set persons({"Jake", "Bob", "Michael", "Philipp"}); + int map_call_count = 0; + + const auto lazy_persons = persons + .lazy() + .map([&map_call_count](const std::string& name) { + ++map_call_count; + return name + "!"; + }); + + const auto lazy_zipped = ages + .lazy() + .zip(lazy_persons); + + EXPECT_EQ(0, map_call_count); + + const auto zipped = lazy_zipped.get(); + + const auto expected = set>({ + std::pair(25, "Bob!"), + std::pair(30, "Jake!"), + std::pair(45, "Michael!"), + std::pair(63, "Philipp!"), + }); + EXPECT_EQ(expected, zipped); + EXPECT_EQ(4, map_call_count); +} + +TEST(SetTest, LazyZipWithDifferentSizesThrows) +{ + const set ages({25, 45}); + const std::set persons({"Jake"}); + + EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons).get(); }, ""); +} From 11b393231f372513b29046767c8523b0ac1b5711 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 21:04:57 +0200 Subject: [PATCH 07/20] added missing lazy_set overload --- README.md | 2 +- include/set.h | 24 ++++++++++++++++++++++++ tests/set_test.cc | 31 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31bbca1..383ce64 100644 --- a/README.md +++ b/README.md @@ -484,7 +484,7 @@ const auto total = numbers // total -> 42 ``` -Lazy set zip can combine a lazy set with an `fcpp::set`, a `std::set`, an `fcpp::vector`, a `std::vector`, or another `fcpp::lazy_set`. Size validation is deferred until a terminal operation is called. When zipping with a vector, duplicate vector values are removed before zipping, just like the eager set zip operation. When zipping with another lazy set, the right-hand lazy set is materialized internally at that point, so its keys can be paired in set order. +Lazy set zip can combine a lazy set with an `fcpp::set`, a `std::set`, an `fcpp::vector`, a `std::vector`, an `fcpp::lazy_vector`, or another `fcpp::lazy_set`. Size validation is deferred until a terminal operation is called. When zipping with a vector, duplicate vector values are removed before zipping, just like the eager set zip operation. When zipping with a lazy vector, the right-hand lazy vector is materialized internally at that point and then deduplicated. When zipping with another lazy set, the right-hand lazy set is materialized internally at that point, so its keys can be paired in set order. ```c++ const fcpp::set ages({25, 45, 30, 63}); diff --git a/include/set.h b/include/set.h index e1674a3..982027c 100644 --- a/include/set.h +++ b/include/set.h @@ -35,6 +35,9 @@ namespace fcpp { template class vector; + template + class lazy_vector; + template class set; @@ -219,6 +222,27 @@ namespace fcpp { return zip(lazy_set(std::move(distinct_values))); } + // Performs the functional `zip` algorithm lazily where the lazy vector is materialized + // and duplicates are removed when a terminal operation is called. The lazy vector must + // contain the same number of distinct values as the set size. + template + [[nodiscard]] lazy_set> zip(const lazy_vector& vector) const + { + const auto previous = m_operation; + return lazy_set>( + [previous, vector](const std::function&)>& consumer) { + const auto materialized_vector = vector.get(); + std::set distinct_values(materialized_vector.begin(), materialized_vector.end()); + auto it = distinct_values.begin(); + previous([&distinct_values, &it, &consumer](const TKey& key) { + assert(it != distinct_values.end()); + consumer({key, *it}); + ++it; + }); + assert(it == distinct_values.end()); + }); + } + // Performs the functional `zip` algorithm lazily, in which every key of the resulting // lazy set is a tuple of this instance's key (first) and the second lazy set's key (second). // The sizes of the two lazy sets must be equal. diff --git a/tests/set_test.cc b/tests/set_test.cc index 2deceb6..711a532 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -711,6 +711,37 @@ TEST(SetTest, LazyZipWithStdVector) EXPECT_EQ(expected, zipped); } +TEST(SetTest, LazyZipWithLazyVector) +{ + const set ages({25, 45, 30, 63}); + const vector persons({"Jake", "Bob", "Michael", "Philipp"}); + int map_call_count = 0; + + const auto lazy_persons = persons + .lazy() + .map([&map_call_count](const std::string& name) { + ++map_call_count; + return name + "!"; + }); + + const auto lazy_zipped = ages + .lazy() + .zip(lazy_persons); + + EXPECT_EQ(0, map_call_count); + + const auto zipped = lazy_zipped.get(); + + const auto expected = set>({ + std::pair(25, "Bob!"), + std::pair(30, "Jake!"), + std::pair(45, "Michael!"), + std::pair(63, "Philipp!"), + }); + EXPECT_EQ(expected, zipped); + EXPECT_EQ(4, map_call_count); +} + TEST(SetTest, LazyZipWithLazySet) { const set ages({25, 45, 30, 63}); From d4abe44686e64772cae94d941d8ad0ab7f36a330 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 21:31:02 +0200 Subject: [PATCH 08/20] lazy_set set algebra added --- README.md | 30 +++++++- include/set.h | 189 ++++++++++++++++++++++++++++++++++++++++++++++ tests/set_test.cc | 180 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 398 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 383ce64..78830cb 100644 --- a/README.md +++ b/README.md @@ -455,7 +455,7 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c ``` ### Lazy operations -Lazy sets are useful when chaining operations over a large set and only needing the final materialized set or a reduced value. A regular `map().filter().reduce()` chain creates intermediate sets and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process keys in one pass. Unlike vectors, sets are already ordered by their comparator, so lazy sets focus on the operations that make sense for set data: `map`, `filter`, `zip`, and `reduce`. +Lazy sets are useful when chaining operations over a large set and only needing the final materialized set or a reduced value. A regular `map().filter().reduce()` chain creates intermediate sets and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process keys in one pass. Unlike vectors, sets are already ordered by their comparator, so lazy sets focus on the operations that make sense for set data: `map`, `filter`, `difference_with`, `union_with`, `intersect_with`, `zip`, and `reduce`. ```c++ #include "set.h" // instead of @@ -484,6 +484,34 @@ const auto total = numbers // total -> 42 ``` +Lazy set algebra can combine a lazy set with an `fcpp::set`, a `std::set`, or another `fcpp::lazy_set`. The operation is still deferred, but set algebra needs set membership and sorted set semantics, so the current lazy pipeline is materialized internally when the terminal operation is called. When the right-hand side is also lazy, it is materialized internally at the same point. + +```c++ +const fcpp::set colleague_ages({15, 18, 25, 41, 51}); +const fcpp::set friend_ages({41, 42, 51}); +const fcpp::set family_ages({51, 81}); + +const auto guests = colleague_ages + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const int& age) { + return age >= 18; + }) + + // set difference is not evaluated yet + .difference_with(friend_ages) + + // set union is not evaluated yet + .union_with(family_ages) + + // terminal operation: the lazy filter and set algebra run here + .get(); + +// guests -> fcpp::set({18, 25, 51, 81}) +``` + Lazy set zip can combine a lazy set with an `fcpp::set`, a `std::set`, an `fcpp::vector`, a `std::vector`, an `fcpp::lazy_vector`, or another `fcpp::lazy_set`. Size validation is deferred until a terminal operation is called. When zipping with a vector, duplicate vector values are removed before zipping, just like the eager set zip operation. When zipping with a lazy vector, the right-hand lazy vector is materialized internally at that point and then deduplicated. When zipping with another lazy set, the right-hand lazy set is materialized internally at that point, so its keys can be paired in set order. ```c++ diff --git a/include/set.h b/include/set.h index 982027c..a719012 100644 --- a/include/set.h +++ b/include/set.h @@ -167,6 +167,195 @@ namespace fcpp { return filter(std::forward(predicate_to_keep)); } + // Returns the lazy set of elements which belong to this lazy set but not in the other set. + // The operation is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set set1({1, 2, 3, 5, 7, 8, 10}); + // const fcpp::set set2({2, 5, 7, 10, 15, 17}); + // const auto diff = set1 + // .lazy() + // .difference_with(set2) + // .get(); + // + // outcome: + // diff -> fcpp::set({1, 3, 8}) + [[nodiscard]] lazy_set difference_with(const set& other) const + { + const auto previous = m_operation; + return lazy_set( + [previous, &other](const std::function& consumer) { + std::set current; + previous([¤t](const TKey& key) { + current.insert(key); + }); + + std::set diff; + std::set_difference(current.begin(), + current.end(), + other.begin(), + other.end(), + std::inserter(diff, diff.begin())); + std::for_each(diff.begin(), diff.end(), consumer); + }); + } + + // Returns the lazy set of elements which belong to this lazy set but not in the std::set. + // The operation is deferred until a terminal operation is called. + [[nodiscard]] lazy_set difference_with(const std::set& other) const + { + return difference_with(lazy_set(other)); + } + + // Returns the lazy set of elements which belong to this lazy set but not in the other lazy set. + // Both lazy sets are materialized internally when a terminal operation is called. + [[nodiscard]] lazy_set difference_with(const lazy_set& other) const + { + const auto previous = m_operation; + return lazy_set( + [previous, other](const std::function& consumer) { + std::set current; + previous([¤t](const TKey& key) { + current.insert(key); + }); + + const auto materialized_other = other.get(); + std::set diff; + std::set_difference(current.begin(), + current.end(), + materialized_other.begin(), + materialized_other.end(), + std::inserter(diff, diff.begin())); + std::for_each(diff.begin(), diff.end(), consumer); + }); + } + + // Returns the lazy set of elements which belong either to this lazy set or the other set. + // The operation is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set set1({1, 2, 3, 5, 7, 8, 10}); + // const fcpp::set set2({2, 5, 7, 10, 15, 17}); + // const auto combined = set1 + // .lazy() + // .union_with(set2) + // .get(); + // + // outcome: + // combined -> fcpp::set({1, 2, 3, 5, 7, 8, 10, 15, 17}) + [[nodiscard]] lazy_set union_with(const set& other) const + { + const auto previous = m_operation; + return lazy_set( + [previous, &other](const std::function& consumer) { + std::set current; + previous([¤t](const TKey& key) { + current.insert(key); + }); + + std::set combined; + std::set_union(current.begin(), + current.end(), + other.begin(), + other.end(), + std::inserter(combined, combined.begin())); + std::for_each(combined.begin(), combined.end(), consumer); + }); + } + + // Returns the lazy set of elements which belong either to this lazy set or the std::set. + // The operation is deferred until a terminal operation is called. + [[nodiscard]] lazy_set union_with(const std::set& other) const + { + return union_with(lazy_set(other)); + } + + // Returns the lazy set of elements which belong either to this lazy set or the other lazy set. + // Both lazy sets are materialized internally when a terminal operation is called. + [[nodiscard]] lazy_set union_with(const lazy_set& other) const + { + const auto previous = m_operation; + return lazy_set( + [previous, other](const std::function& consumer) { + std::set current; + previous([¤t](const TKey& key) { + current.insert(key); + }); + + const auto materialized_other = other.get(); + std::set combined; + std::set_union(current.begin(), + current.end(), + materialized_other.begin(), + materialized_other.end(), + std::inserter(combined, combined.begin())); + std::for_each(combined.begin(), combined.end(), consumer); + }); + } + + // Returns the lazy set of elements which belong to both this lazy set and the other set. + // The operation is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set set1({1, 2, 3, 5, 7, 8, 10}); + // const fcpp::set set2({2, 5, 7, 10, 15, 17}); + // const auto combined = set1 + // .lazy() + // .intersect_with(set2) + // .get(); + // + // outcome: + // combined -> fcpp::set({2, 5, 7, 10}) + [[nodiscard]] lazy_set intersect_with(const set& other) const + { + const auto previous = m_operation; + return lazy_set( + [previous, &other](const std::function& consumer) { + std::set current; + previous([¤t](const TKey& key) { + current.insert(key); + }); + + std::set intersection; + std::set_intersection(current.begin(), + current.end(), + other.begin(), + other.end(), + std::inserter(intersection, intersection.begin())); + std::for_each(intersection.begin(), intersection.end(), consumer); + }); + } + + // Returns the lazy set of elements which belong to both this lazy set and the std::set. + // The operation is deferred until a terminal operation is called. + [[nodiscard]] lazy_set intersect_with(const std::set& other) const + { + return intersect_with(lazy_set(other)); + } + + // Returns the lazy set of elements which belong to both this lazy set and the other lazy set. + // Both lazy sets are materialized internally when a terminal operation is called. + [[nodiscard]] lazy_set intersect_with(const lazy_set& other) const + { + const auto previous = m_operation; + return lazy_set( + [previous, other](const std::function& consumer) { + std::set current; + previous([¤t](const TKey& key) { + current.insert(key); + }); + + const auto materialized_other = other.get(); + std::set intersection; + std::set_intersection(current.begin(), + current.end(), + materialized_other.begin(), + materialized_other.end(), + std::inserter(intersection, intersection.begin())); + std::for_each(intersection.begin(), intersection.end(), consumer); + }); + } + // Performs the functional `zip` algorithm lazily, in which every key of the resulting // lazy set is a tuple of this instance's key (first) and the second set's key (second). // The sizes of the two sets must be equal. diff --git a/tests/set_test.cc b/tests/set_test.cc index 711a532..7dc86ff 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -780,3 +780,183 @@ TEST(SetTest, LazyZipWithDifferentSizesThrows) EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons).get(); }, ""); } + +TEST(SetTest, LazyDifferenceWithFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({2, 5, 7, 10, 15, 17}); + int filter_call_count = 0; + + const auto lazy_diff = set1 + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value < 10; + }) + .difference_with(set2); + + EXPECT_EQ(0, filter_call_count); + + const auto diff = lazy_diff.get(); + + EXPECT_EQ(set({1, 3, 8}), diff); + EXPECT_EQ(7, filter_call_count); +} + +TEST(SetTest, LazyDifferenceWithStdSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const std::set set2({2, 5, 7, 10, 15, 17}); + + const auto diff = set1 + .lazy() + .difference_with(set2) + .get(); + + EXPECT_EQ(set({1, 3, 8}), diff); +} + +TEST(SetTest, LazyDifferenceWithLazySet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({1, 2, 3, 5, 7, 8}); + int map_call_count = 0; + + const auto lazy_set2 = set2 + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value + 2; + }); + + const auto lazy_diff = set1 + .lazy() + .difference_with(lazy_set2); + + EXPECT_EQ(0, map_call_count); + + const auto diff = lazy_diff.get(); + + EXPECT_EQ(set({1, 2, 8}), diff); + EXPECT_EQ(6, map_call_count); +} + +TEST(SetTest, LazyUnionWithFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({2, 5, 7, 10, 15, 17}); + int filter_call_count = 0; + + const auto lazy_combined = set1 + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value < 10; + }) + .union_with(set2); + + EXPECT_EQ(0, filter_call_count); + + const auto combined = lazy_combined.get(); + + EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), combined); + EXPECT_EQ(7, filter_call_count); +} + +TEST(SetTest, LazyUnionWithStdSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const std::set set2({2, 5, 7, 10, 15, 17}); + + const auto combined = set1 + .lazy() + .union_with(set2) + .get(); + + EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), combined); +} + +TEST(SetTest, LazyUnionWithLazySet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({1, 2, 3, 5, 7, 8}); + int map_call_count = 0; + + const auto lazy_set2 = set2 + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value + 2; + }); + + const auto lazy_combined = set1 + .lazy() + .union_with(lazy_set2); + + EXPECT_EQ(0, map_call_count); + + const auto combined = lazy_combined.get(); + + EXPECT_EQ(set({1, 2, 3, 4, 5, 7, 8, 9, 10}), combined); + EXPECT_EQ(6, map_call_count); +} + +TEST(SetTest, LazyIntersectionWithFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({2, 5, 7, 10, 15, 17}); + int filter_call_count = 0; + + const auto lazy_intersection = set1 + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value < 10; + }) + .intersect_with(set2); + + EXPECT_EQ(0, filter_call_count); + + const auto intersection = lazy_intersection.get(); + + EXPECT_EQ(set({2, 5, 7}), intersection); + EXPECT_EQ(7, filter_call_count); +} + +TEST(SetTest, LazyIntersectionWithStdSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const std::set set2({2, 5, 7, 10, 15, 17}); + + const auto intersection = set1 + .lazy() + .intersect_with(set2) + .get(); + + EXPECT_EQ(set({2, 5, 7, 10}), intersection); +} + +TEST(SetTest, LazyIntersectionWithLazySet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({1, 2, 3, 5, 7, 8}); + int map_call_count = 0; + + const auto lazy_set2 = set2 + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value + 2; + }); + + const auto lazy_intersection = set1 + .lazy() + .intersect_with(lazy_set2); + + EXPECT_EQ(0, map_call_count); + + const auto intersection = lazy_intersection.get(); + + EXPECT_EQ(set({3, 5, 7, 10}), intersection); + EXPECT_EQ(6, map_call_count); +} From 77832614427b3cd945e08eedcfbeb533bc50da0d Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 21:43:13 +0200 Subject: [PATCH 09/20] added lazy map implementation --- README.md | 51 +++++++++++++ include/map.h | 190 ++++++++++++++++++++++++++++++++++++++++++++++ tests/map_test.cc | 76 +++++++++++++++++++ 3 files changed, 317 insertions(+) diff --git a/README.md b/README.md index 78830cb..1c7e055 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,57 @@ adults.for_each([](const std::pair& element) { }); ``` +### Lazy operations +Lazy maps are useful when chaining `map_to`, `filter`, and `reduce` over a large map. A regular `filtered().map_to().reduce()` style chain creates intermediate maps and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map_to/filter/reduce-style pipelines process key/value pairs in one pass. When a lazy `map_to` creates equivalent output keys, the first key/value pair encountered in sorted map order is kept, following `std::map::insert` semantics. + +```c++ +#include "map.h" // instead of + +const fcpp::map ages({ + {"jake", 32}, + {"mary", 16}, + {"david", 40} +}); + +const auto ages_by_initial = ages + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const std::pair& element) { + return element.second >= 18; + }) + + // this transform is not evaluated yet + .map_to([](const std::pair& element) { + return std::make_pair(element.first[0], std::to_string(element.second) + " years"); + }) + + // terminal operation: all stored operations are executed here + .get(); + +// ages_by_initial -> fcpp::map({{'d', "40 years"}, {'j', "32 years"}}) +// ages -> fcpp::map({{"david", 40}, {"jake", 32}, {"mary", 16}}) +``` + +```c++ +const auto total_age = ages + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const std::pair& element) { + return element.second >= 18; + }) + + // terminal operation: all stored operations are executed here + .reduce(0, [](const int& partial_sum, const std::pair& element) { + return partial_sum + element.second; + }); + +// total_age -> 72 +``` + ### all_of, any_of, none_of ```c++ #include "map.h" // instead of diff --git a/include/map.h b/include/map.h index 775955d..833083f 100644 --- a/include/map.h +++ b/include/map.h @@ -23,13 +23,186 @@ #pragma once #include #include +#include #include +#include #include #include #include "vector.h" namespace fcpp { +template +class map; + +// A lightweight wrapper representing a deferred map pipeline, enabling fluent and functional +// programming while avoiding intermediate map materialization. +// +// Member functions are non-mutating and keep extending the pipeline. Terminal functions such as +// `get` and `reduce` execute the stored operations. +template > +class lazy_map +{ +public: + using value_type = std::pair; + + lazy_map() + : m_operation([](const std::function&) {}) + { + } + + // Creates a lazy map by copying the provided std::map as an owned source. + explicit lazy_map(const std::map& map) + { + auto source = std::make_shared>(map); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy map by moving the provided std::map as an owned source. + explicit lazy_map(std::map&& map) + { + auto source = std::make_shared>(std::move(map)); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy map by referring to an existing std::map source. + // The referenced map must outlive this lazy map. + explicit lazy_map(const std::map* map) + { + m_operation = [map](const std::function& consumer) { + std::for_each(map->begin(), map->end(), consumer); + }; + } + + // Creates a lazy map by directly providing the deferred operation. + // This constructor is mostly useful for composing lazy_map instances. + explicit lazy_map(std::function&)> operation) + : m_operation(std::move(operation)) + { + } + + // Performs the functional `map_to` algorithm lazily. The transform is not applied until + // a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::map ages({{"jake", 32}, {"mary", 26}, {"david", 40}}); + // const auto labels_by_initial = ages + // .lazy() + // .map_to([](const auto& element) { + // return std::make_pair(element.first[0], std::to_string(element.second) + " years"); + // }) + // .get(); + // + // outcome: + // labels_by_initial -> fcpp::map({ + // {'d', "40 years"}, {'j', "32 years"}, {'m', "26 years"} + // }) +#ifdef CPP17_AVAILABLE + template , Transform, value_type>>> +#else + template +#endif + [[nodiscard]] lazy_map map_to(Transform&& transform) const + { + const auto previous = m_operation; + typename std::decay::type transform_copy(std::forward(transform)); + return lazy_map( + [previous, transform_copy](const std::function::value_type&)>& consumer) mutable { + previous([&consumer, &transform_copy](const value_type& element) { + const auto transformed = transform_copy(element); + const typename lazy_map::value_type transformed_element(transformed.first, transformed.second); + consumer(transformed_element); + }); + }); + } + + // Performs the functional `filter` algorithm lazily, in which all key/value pairs which match + // the given predicate are kept. The predicate is not applied until a terminal operation, + // such as `get` or `reduce`, is called. + // + // example: + // const fcpp::map ages({{"jake", 32}, {"mary", 26}, {"david", 40}}); + // const auto adults = ages + // .lazy() + // .filter([](const auto& element) { + // return element.second >= 32; + // }) + // .get(); + // + // outcome: + // adults -> fcpp::map({{"david", 40}, {"jake", 32}}) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_map filter(Filter&& predicate_to_keep) const + { + const auto previous = m_operation; + typename std::decay::type predicate_copy(std::forward(predicate_to_keep)); + return lazy_map( + [previous, predicate_copy](const std::function& consumer) mutable { + previous([&consumer, &predicate_copy](const value_type& element) { + if (predicate_copy(element)) { + consumer(element); + } + }); + }); + } + + // Performs the functional `filter` algorithm lazily. + // See also `filter` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_map filtered(Filter&& predicate_to_keep) const + { + return filter(std::forward(predicate_to_keep)); + } + + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of + // accumulating all key/value pairs in this lazy map to an initial value. + // + // example: + // const fcpp::map ages({{"jake", 32}, {"mary", 26}, {"david", 40}}); + // const auto total_age = ages + // .lazy() + // .filter([](const auto& element) { + // return element.second >= 32; + // }) + // .reduce(0, [](const int& partial_sum, const auto& element) { + // return partial_sum + element.second; + // }); + // + // outcome: + // total_age -> 72 +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + U reduce(const U& initial, Reduce&& reduction) const + { + auto result = initial; + m_operation([&result, &reduction](const value_type& element) { + result = reduction(result, element); + }); + return result; + } + + // Materializes this lazy map to a functional map, executing all stored operations. + [[nodiscard]] map get() const; + +private: + std::function&)> m_operation; +}; + // A lightweight wrapper around std::map, enabling fluent and functional // programming on the map itself, rather than using the more procedural style // of the standard library algorithms. @@ -436,6 +609,13 @@ class map return m_map.size(); } + // Starts a lazy pipeline. The returned lazy map defers following map_to/filter transformations + // until a terminal operation, such as get() or reduce(), is called. + [[nodiscard]] lazy_map lazy() const + { + return lazy_map(&m_map); + } + // Returns the begin iterator, useful for other standard library algorithms [[nodiscard]] typename std::map::iterator begin() { @@ -511,4 +691,14 @@ class map std::map m_map; }; +template +[[nodiscard]] map lazy_map::get() const +{ + std::map materialized; + m_operation([&materialized](const value_type& element) { + materialized.insert(element); + }); + return map(std::move(materialized)); +} + } diff --git a/tests/map_test.cc b/tests/map_test.cc index 43df882..65219c8 100644 --- a/tests/map_test.cc +++ b/tests/map_test.cc @@ -247,3 +247,79 @@ TEST(MapTest, InequalityOperator) EXPECT_FALSE(map1 == map2); EXPECT_TRUE(map1 != map2); } + +TEST(MapTest, LazyMapToFilterGet) +{ + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto lazy_persons = persons + .lazy() + .map_to([&map_call_count](const std::pair& element) { + ++map_call_count; + return std::make_pair(element.first[0], std::to_string(element.second) + " years"); + }) + .filter([&filter_call_count](const std::pair& element) { + ++filter_call_count; + return element.first != 'm'; + }); + + EXPECT_EQ(0, map_call_count); + EXPECT_EQ(0, filter_call_count); + + const auto mapped = lazy_persons.get(); + + EXPECT_EQ((map({{'d', "40 years"}, {'j', "32 years"}})), mapped); + EXPECT_EQ((map({{"david", 40}, {"jake", 32}, {"mary", 26}})), persons); + EXPECT_EQ(3, map_call_count); + EXPECT_EQ(3, filter_call_count); +} + +TEST(MapTest, LazyFiltered) +{ + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + + const auto filtered_persons = persons + .lazy() + .filtered([](const std::pair& element) { + return element.second >= 32; + }) + .get(); + + EXPECT_EQ((map({{"david", 40}, {"jake", 32}})), filtered_persons); + EXPECT_EQ((map({{"david", 40}, {"jake", 32}, {"mary", 26}})), persons); +} + +TEST(MapTest, LazyReduce) +{ + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + int filter_call_count = 0; + + const auto total_age = persons + .lazy() + .filter([&filter_call_count](const std::pair& element) { + ++filter_call_count; + return element.second >= 32; + }) + .reduce(0, [](const int& partial_sum, const std::pair& element) { + return partial_sum + element.second; + }); + + EXPECT_EQ(72, total_age); + EXPECT_EQ(3, filter_call_count); +} + +TEST(MapTest, LazyMapToDuplicateKeysKeepsFirst) +{ + const map persons({{"anna", 28}, {"alex", 30}, {"david", 40}}); + + const auto mapped = persons + .lazy() + .map_to([](const std::pair& element) { + return std::make_pair(element.first[0], element.second); + }) + .get(); + + EXPECT_EQ((map({{'a', 30}, {'d', 40}})), mapped); +} From a992870d04f34e7cf567ee0f0d58f0143680284f Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 21:47:07 +0200 Subject: [PATCH 10/20] Added contents table in README --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 1c7e055..b4d6ba3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,36 @@ The primary focus of this library is * encapsulation of the iterator madness * removal of manual for-loops +## Contents +* [Compilation (Cmake)](#compilation-cmake) + * [Dependencies](#dependencies) + * [Minimum C++ version](#minimum-c-version) + * [macOS (Xcode)](#macos-xcode) + * [macOS (Makefiles/clang)](#macos-makefilesclang) + * [macOS (Makefiles/g++)](#macos-makefilesg) + * [Linux (Makefiles)](#linux-makefiles) + * [Windows (Visual Studio)](#windows-visual-studio) +* [Functional vector usage (fcpp::vector)](#functional-vector-usage-fcppvector) + * [extract unique (distinct) elements in a set](#extract-unique-distinct-elements-in-a-set) + * [zip, map, filter, sort, reduce](#zip-map-filter-sort-reduce) + * [Lazy operations](#lazy-operations) + * [index search](#index-search) + * [remove, insert](#remove-insert) + * [size, capacity, reserve, resize](#size-capacity-reserve-resize) + * [all_of, any_of, none_of](#all_of-any_of-none_of) + * [Parallel algorithms](#parallel-algorithms) +* [Functional set usage (fcpp::set)](#functional-set-usage-fcppset) + * [difference, union, intersection](#difference-union-intersection-works-with-fcppset-and-stdset) + * [zip, map, filter, reduce](#zip-map-filter-reduce) + * [Lazy operations](#lazy-operations-1) + * [all_of, any_of, none_of](#all_of-any_of-none_of-1) + * [remove, insert, contains, size, clear](#remove-insert-contains-size-clear) +* [Functional map usage (fcpp::map)](#functional-map-usage-fcppmap) + * [map_to, filter, reduce, for_each](#map_to-filter-reduce-for_each) + * [Lazy operations](#lazy-operations-2) + * [all_of, any_of, none_of](#all_of-any_of-none_of-2) + * [keys, values, remove, insert](#keys-values-remove-insert) + ## Compilation (Cmake) ### Dependencies * CMake >= 3.14 From d6f2880c7651576e6e2e63755491beb397c53d40 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 21:48:45 +0200 Subject: [PATCH 11/20] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b4d6ba3..2bdf9c9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The primary focus of this library is * [Functional vector usage (fcpp::vector)](#functional-vector-usage-fcppvector) * [extract unique (distinct) elements in a set](#extract-unique-distinct-elements-in-a-set) * [zip, map, filter, sort, reduce](#zip-map-filter-sort-reduce) - * [Lazy operations](#lazy-operations) + * [lazy operations](#lazy-operations) * [index search](#index-search) * [remove, insert](#remove-insert) * [size, capacity, reserve, resize](#size-capacity-reserve-resize) @@ -31,12 +31,12 @@ The primary focus of this library is * [Functional set usage (fcpp::set)](#functional-set-usage-fcppset) * [difference, union, intersection](#difference-union-intersection-works-with-fcppset-and-stdset) * [zip, map, filter, reduce](#zip-map-filter-reduce) - * [Lazy operations](#lazy-operations-1) + * [lazy operations](#lazy-operations-1) * [all_of, any_of, none_of](#all_of-any_of-none_of-1) * [remove, insert, contains, size, clear](#remove-insert-contains-size-clear) * [Functional map usage (fcpp::map)](#functional-map-usage-fcppmap) * [map_to, filter, reduce, for_each](#map_to-filter-reduce-for_each) - * [Lazy operations](#lazy-operations-2) + * [lazy operations](#lazy-operations-2) * [all_of, any_of, none_of](#all_of-any_of-none_of-2) * [keys, values, remove, insert](#keys-values-remove-insert) @@ -166,7 +166,7 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c }); ``` -### Lazy operations +### lazy operations Lazy vectors are useful when chaining multiple operations over a large vector. A regular `map().filter().reduce()` style chain creates intermediate vectors and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is an important exception: it cannot be streamed element by element, so lazy `sort`, `sort_ascending`, and `sort_descending` first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain. ```c++ @@ -484,7 +484,7 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c }); ``` -### Lazy operations +### lazy operations Lazy sets are useful when chaining operations over a large set and only needing the final materialized set or a reduced value. A regular `map().filter().reduce()` chain creates intermediate sets and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process keys in one pass. Unlike vectors, sets are already ordered by their comparator, so lazy sets focus on the operations that make sense for set data: `map`, `filter`, `difference_with`, `union_with`, `intersect_with`, `zip`, and `reduce`. ```c++ @@ -670,7 +670,7 @@ adults.for_each([](const std::pair& element) { }); ``` -### Lazy operations +### lazy operations Lazy maps are useful when chaining `map_to`, `filter`, and `reduce` over a large map. A regular `filtered().map_to().reduce()` style chain creates intermediate maps and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map_to/filter/reduce-style pipelines process key/value pairs in one pass. When a lazy `map_to` creates equivalent output keys, the first key/value pair encountered in sorted map order is kept, following `std::map::insert` semantics. ```c++ From 8516612aaa423ea17c1ea781547684b7df8c0005 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 22:00:13 +0200 Subject: [PATCH 12/20] fixed header ordering --- include/export_def.h | 2 -- include/map.h | 2 ++ include/optional.h | 10 +++++++--- include/set.h | 2 ++ include/vector.h | 15 ++++++++++----- tests/map_test.cc | 8 ++++++-- tests/set_test.cc | 12 ++++++++++-- tests/test_types.h | 3 +++ tests/vector_test.cc | 10 ++++++++-- 9 files changed, 48 insertions(+), 16 deletions(-) diff --git a/include/export_def.h b/include/export_def.h index dcca3a3..2f306f1 100644 --- a/include/export_def.h +++ b/include/export_def.h @@ -31,5 +31,3 @@ #else #define FunctionalCppExport __attribute__ ((__visibility__("default"))) #endif - -#include "compatibility.h" diff --git a/include/map.h b/include/map.h index 833083f..f8b66ae 100644 --- a/include/map.h +++ b/include/map.h @@ -23,7 +23,9 @@ #pragma once #include #include +#include #include +#include #include #include #include diff --git a/include/optional.h b/include/optional.h index 8fd62f7..81c5946 100644 --- a/include/optional.h +++ b/include/optional.h @@ -24,15 +24,19 @@ #pragma once #include "compatibility.h" -namespace fcpp { #ifdef CPP17_AVAILABLE #include -template -using optional_t = std::optional; #else +#include #include #include +#endif +namespace fcpp { +#ifdef CPP17_AVAILABLE +template +using optional_t = std::optional; +#else // A replacement for std::optional when C++17 is not available template class optional diff --git a/include/set.h b/include/set.h index a719012..74a84dd 100644 --- a/include/set.h +++ b/include/set.h @@ -23,7 +23,9 @@ #pragma once #include #include +#include #include +#include #include #include #include diff --git a/include/vector.h b/include/vector.h index 5032248..7d90bfb 100644 --- a/include/vector.h +++ b/include/vector.h @@ -21,19 +21,24 @@ // SOFTWARE. #pragma once +#include "compatibility.h" + #include #include +#include +#ifdef PARALLEL_ALGORITHM_AVAILABLE +#include +#endif #include -#include -#include +#include #include #include +#include #include +#include + #include "index_range.h" #include "optional.h" -#ifdef PARALLEL_ALGORITHM_AVAILABLE -#include -#endif namespace fcpp { template diff --git a/tests/map_test.cc b/tests/map_test.cc index 65219c8..f1da71e 100644 --- a/tests/map_test.cc +++ b/tests/map_test.cc @@ -20,10 +20,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include +#include +#include + #include -#include "warnings.h" + #include "map.h" -#include +#include "warnings.h" using namespace fcpp; diff --git a/tests/set_test.cc b/tests/set_test.cc index 7dc86ff..6ec4a5c 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -20,11 +20,19 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include +#include +#include +#include +#include +#include + #include -#include "warnings.h" + #include "set.h" -#include "vector.h" #include "test_types.h" +#include "vector.h" +#include "warnings.h" using namespace fcpp; diff --git a/tests/test_types.h b/tests/test_types.h index 3205945..361688f 100644 --- a/tests/test_types.h +++ b/tests/test_types.h @@ -21,7 +21,10 @@ // SOFTWARE. #pragma once +#include +#include #include +#include struct child { diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 2ac49aa..730bc87 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -20,11 +20,17 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include +#include +#include +#include + #include -#include "vector.h" -#include "set.h" + #include "index_range.h" +#include "set.h" #include "test_types.h" +#include "vector.h" #include "warnings.h" #pragma warning( push ) From 5c35fd9cdf242c11bd216ddb44414a655525295a Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 22:04:36 +0200 Subject: [PATCH 13/20] header cleanups --- include/map.h | 2 ++ include/set.h | 3 +++ tests/vector_test.cc | 1 + 3 files changed, 6 insertions(+) diff --git a/include/map.h b/include/map.h index f8b66ae..681fdb5 100644 --- a/include/map.h +++ b/include/map.h @@ -21,6 +21,8 @@ // SOFTWARE. #pragma once +#include "compatibility.h" + #include #include #include diff --git a/include/set.h b/include/set.h index 74a84dd..81616ca 100644 --- a/include/set.h +++ b/include/set.h @@ -21,11 +21,14 @@ // SOFTWARE. #pragma once +#include "compatibility.h" + #include #include #include #include #include +#include #include #include #include diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 730bc87..732f5b8 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -20,6 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include #include #include #include From c14c7b47422c55a57a9ffc3e2ad72ee6be91cae7 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 3 May 2026 22:38:08 +0200 Subject: [PATCH 14/20] Fixed PR comments --- include/vector.h | 34 +++++++++-------------- tests/vector_test.cc | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/include/vector.h b/include/vector.h index 7d90bfb..ec8b19c 100644 --- a/include/vector.h +++ b/include/vector.h @@ -82,16 +82,6 @@ namespace fcpp { }; } - // Creates a lazy vector by referring to an existing std::vector source. - // The referenced vector must outlive this lazy vector. - explicit lazy_vector(const std::vector* vector) - : m_capacity_hint(vector->size()) - { - m_operation = [vector](const std::function& consumer) { - std::for_each(vector->begin(), vector->end(), consumer); - }; - } - // Creates a lazy vector by directly providing the deferred operation. // This constructor is mostly useful for composing lazy_vector instances. lazy_vector(std::function&)> operation, size_t capacity_hint) @@ -216,15 +206,16 @@ namespace fcpp { { const auto previous = m_operation; const auto capacity_hint = m_capacity_hint; + const auto vector_copy = vector; return lazy_vector>( - [previous, &vector](const std::function&)>& consumer) { + [previous, vector_copy](const std::function&)>& consumer) { size_t index = 0; - previous([&vector, &consumer, &index](const T& element) { - assert(index < vector.size()); - consumer({element, vector[index]}); + previous([&vector_copy, &consumer, &index](const T& element) { + assert(index < vector_copy.size()); + consumer({element, vector_copy[index]}); ++index; }); - assert(index == vector.size()); + assert(index == vector_copy.size()); }, capacity_hint); } @@ -237,15 +228,16 @@ namespace fcpp { { const auto previous = m_operation; const auto capacity_hint = m_capacity_hint; + const auto vector_copy = vector; return lazy_vector>( - [previous, &vector](const std::function&)>& consumer) { + [previous, vector_copy](const std::function&)>& consumer) { size_t index = 0; - previous([&vector, &consumer, &index](const T& element) { - assert(index < vector.size()); - consumer({element, vector[index]}); + previous([&vector_copy, &consumer, &index](const T& element) { + assert(index < vector_copy.size()); + consumer({element, vector_copy[index]}); ++index; }); - assert(index == vector.size()); + assert(index == vector_copy.size()); }, capacity_hint); } @@ -1785,7 +1777,7 @@ namespace fcpp { // transformations until a terminal operation, such as get() or reduce(), is called. [[nodiscard]] lazy_vector lazy() const { - return lazy_vector(&m_vector); + return lazy_vector(m_vector); } // Returns the begin iterator, useful for other standard library algorithms diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 732f5b8..cc9dcdc 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -1448,6 +1448,32 @@ TEST(VectorTest, LazyReduce) EXPECT_EQ(10, filter_call_count); } +TEST(VectorTest, LazySourceCanOutliveFunctionalVector) +{ + lazy_vector lazy_numbers; + { + const vector vector_under_test({1, 2, 3, 4}); + lazy_numbers = vector_under_test + .lazy() + .filter([](const int& value) { + return value > 2; + }); + } + + EXPECT_EQ(vector({3, 4}), lazy_numbers.get()); +} + +TEST(VectorTest, LazySourceCanStartFromTemporaryFunctionalVector) +{ + const auto lazy_numbers = vector({1, 2, 3, 4}) + .lazy() + .map([](const int& value) { + return value * 2; + }); + + EXPECT_EQ(vector({2, 4, 6, 8}), lazy_numbers.get()); +} + TEST(VectorTest, LazySort) { const vector vector_under_test({ @@ -1577,6 +1603,44 @@ TEST(VectorTest, LazyZipWithStdVector) EXPECT_EQ("three", zipped_vector[2].second); } +TEST(VectorTest, LazyZipWithTemporaryFunctionalVector) +{ + const vector vector_under_test({1, 2, 3}); + + const auto lazy_vector = vector_under_test + .lazy() + .zip(vector({"one", "two", "three"})); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(1, zipped_vector[0].first); + EXPECT_EQ("one", zipped_vector[0].second); + EXPECT_EQ(2, zipped_vector[1].first); + EXPECT_EQ("two", zipped_vector[1].second); + EXPECT_EQ(3, zipped_vector[2].first); + EXPECT_EQ("three", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithTemporaryStdVector) +{ + const vector vector_under_test({1, 2, 3}); + + const auto lazy_vector = vector_under_test + .lazy() + .zip(std::vector({"one", "two", "three"})); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(1, zipped_vector[0].first); + EXPECT_EQ("one", zipped_vector[0].second); + EXPECT_EQ(2, zipped_vector[1].first); + EXPECT_EQ("two", zipped_vector[1].second); + EXPECT_EQ(3, zipped_vector[2].first); + EXPECT_EQ("three", zipped_vector[2].second); +} + TEST(VectorTest, LazyZipWithLazyVector) { const vector ages({32, 45, 37}); From d85e43fa701255406d9ab426b325fc564172dc02 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 4 May 2026 07:32:53 +0200 Subject: [PATCH 15/20] Fixed lazy_set and lazy_map ownership --- include/map.h | 11 +----- include/set.h | 86 +++---------------------------------------- tests/map_test.cc | 26 +++++++++++++ tests/set_test.cc | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 90 deletions(-) diff --git a/include/map.h b/include/map.h index 681fdb5..b8c804c 100644 --- a/include/map.h +++ b/include/map.h @@ -73,15 +73,6 @@ class lazy_map }; } - // Creates a lazy map by referring to an existing std::map source. - // The referenced map must outlive this lazy map. - explicit lazy_map(const std::map* map) - { - m_operation = [map](const std::function& consumer) { - std::for_each(map->begin(), map->end(), consumer); - }; - } - // Creates a lazy map by directly providing the deferred operation. // This constructor is mostly useful for composing lazy_map instances. explicit lazy_map(std::function&)> operation) @@ -617,7 +608,7 @@ class map // until a terminal operation, such as get() or reduce(), is called. [[nodiscard]] lazy_map lazy() const { - return lazy_map(&m_map); + return lazy_map(m_map); } // Returns the begin iterator, useful for other standard library algorithms diff --git a/include/set.h b/include/set.h index 81616ca..3642ef4 100644 --- a/include/set.h +++ b/include/set.h @@ -78,15 +78,6 @@ namespace fcpp { }; } - // Creates a lazy set by referring to an existing std::set source. - // The referenced set must outlive this lazy set. - explicit lazy_set(const std::set* set) - { - m_operation = [set](const std::function& consumer) { - std::for_each(set->begin(), set->end(), consumer); - }; - } - // Creates a lazy set by directly providing the deferred operation. // This constructor is mostly useful for composing lazy_set instances. explicit lazy_set(std::function&)> operation) @@ -187,22 +178,7 @@ namespace fcpp { // diff -> fcpp::set({1, 3, 8}) [[nodiscard]] lazy_set difference_with(const set& other) const { - const auto previous = m_operation; - return lazy_set( - [previous, &other](const std::function& consumer) { - std::set current; - previous([¤t](const TKey& key) { - current.insert(key); - }); - - std::set diff; - std::set_difference(current.begin(), - current.end(), - other.begin(), - other.end(), - std::inserter(diff, diff.begin())); - std::for_each(diff.begin(), diff.end(), consumer); - }); + return difference_with(lazy_set(std::set(other.begin(), other.end()))); } // Returns the lazy set of elements which belong to this lazy set but not in the std::set. @@ -250,22 +226,7 @@ namespace fcpp { // combined -> fcpp::set({1, 2, 3, 5, 7, 8, 10, 15, 17}) [[nodiscard]] lazy_set union_with(const set& other) const { - const auto previous = m_operation; - return lazy_set( - [previous, &other](const std::function& consumer) { - std::set current; - previous([¤t](const TKey& key) { - current.insert(key); - }); - - std::set combined; - std::set_union(current.begin(), - current.end(), - other.begin(), - other.end(), - std::inserter(combined, combined.begin())); - std::for_each(combined.begin(), combined.end(), consumer); - }); + return union_with(lazy_set(std::set(other.begin(), other.end()))); } // Returns the lazy set of elements which belong either to this lazy set or the std::set. @@ -313,22 +274,7 @@ namespace fcpp { // combined -> fcpp::set({2, 5, 7, 10}) [[nodiscard]] lazy_set intersect_with(const set& other) const { - const auto previous = m_operation; - return lazy_set( - [previous, &other](const std::function& consumer) { - std::set current; - previous([¤t](const TKey& key) { - current.insert(key); - }); - - std::set intersection; - std::set_intersection(current.begin(), - current.end(), - other.begin(), - other.end(), - std::inserter(intersection, intersection.begin())); - std::for_each(intersection.begin(), intersection.end(), consumer); - }); + return intersect_with(lazy_set(std::set(other.begin(), other.end()))); } // Returns the lazy set of elements which belong to both this lazy set and the std::set. @@ -367,17 +313,7 @@ namespace fcpp { template [[nodiscard]] lazy_set> zip(const set& set) const { - const auto previous = m_operation; - return lazy_set>( - [previous, &set](const std::function&)>& consumer) { - size_t index = 0; - previous([&set, &consumer, &index](const TKey& key) { - assert(index < set.size()); - consumer({key, set[index]}); - ++index; - }); - assert(index == set.size()); - }); + return zip(lazy_set(std::set(set.begin(), set.end()))); } // Performs the functional `zip` algorithm lazily. @@ -385,17 +321,7 @@ namespace fcpp { template [[nodiscard]] lazy_set> zip(const std::set& set) const { - const auto previous = m_operation; - return lazy_set>( - [previous, &set](const std::function&)>& consumer) { - auto it = set.begin(); - previous([&set, &it, &consumer](const TKey& key) { - assert(it != set.end()); - consumer({key, *it}); - ++it; - }); - assert(it == set.end()); - }); + return zip(lazy_set(set)); } // Performs the functional `zip` algorithm lazily where duplicates are removed before zipping. @@ -1082,7 +1008,7 @@ namespace fcpp { // transformations until a terminal operation, such as get() or reduce(), is called. [[nodiscard]] lazy_set lazy() const { - return lazy_set(&m_set); + return lazy_set(m_set); } // Returns the begin iterator, useful for other standard library algorithms diff --git a/tests/map_test.cc b/tests/map_test.cc index f1da71e..178b746 100644 --- a/tests/map_test.cc +++ b/tests/map_test.cc @@ -314,6 +314,32 @@ TEST(MapTest, LazyReduce) EXPECT_EQ(3, filter_call_count); } +TEST(MapTest, LazySourceCanOutliveFunctionalMap) +{ + lazy_map lazy_persons; + { + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + lazy_persons = persons + .lazy() + .filter([](const std::pair& element) { + return element.second >= 32; + }); + } + + EXPECT_EQ((map({{"david", 40}, {"jake", 32}})), lazy_persons.get()); +} + +TEST(MapTest, LazySourceCanStartFromTemporaryFunctionalMap) +{ + const auto lazy_persons = map({{"jake", 32}, {"mary", 26}}) + .lazy() + .map_to([](const std::pair& element) { + return std::make_pair(element.first[0], element.second); + }); + + EXPECT_EQ((map({{'j', 32}, {'m', 26}})), lazy_persons.get()); +} + TEST(MapTest, LazyMapToDuplicateKeysKeepsFirst) { const map persons({{"anna", 28}, {"alex", 30}, {"david", 40}}); diff --git a/tests/set_test.cc b/tests/set_test.cc index 6ec4a5c..842524e 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -719,6 +719,66 @@ TEST(SetTest, LazyZipWithStdVector) EXPECT_EQ(expected, zipped); } +TEST(SetTest, LazySourceCanOutliveFunctionalSet) +{ + lazy_set lazy_numbers; + { + const set numbers({1, 2, 3, 4}); + lazy_numbers = numbers + .lazy() + .filter([](const int& value) { + return value > 2; + }); + } + + EXPECT_EQ(set({3, 4}), lazy_numbers.get()); +} + +TEST(SetTest, LazySourceCanStartFromTemporaryFunctionalSet) +{ + const auto lazy_numbers = set({1, 2, 3, 4}) + .lazy() + .map([](const int& value) { + return value * 2; + }); + + EXPECT_EQ(set({2, 4, 6, 8}), lazy_numbers.get()); +} + +TEST(SetTest, LazyZipWithTemporaryFunctionalSet) +{ + const set ages({25, 45, 30, 63}); + + const auto lazy_zipped = ages + .lazy() + .zip(set({"Jake", "Bob", "Michael", "Philipp"})); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, lazy_zipped.get()); +} + +TEST(SetTest, LazyZipWithTemporaryStdSet) +{ + const set ages({25, 45, 30, 63}); + + const auto lazy_zipped = ages + .lazy() + .zip(std::set({"Jake", "Bob", "Michael", "Philipp"})); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, lazy_zipped.get()); +} + TEST(SetTest, LazyZipWithLazyVector) { const set ages({25, 45, 30, 63}); @@ -849,6 +909,17 @@ TEST(SetTest, LazyDifferenceWithLazySet) EXPECT_EQ(6, map_call_count); } +TEST(SetTest, LazyDifferenceWithTemporaryFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + + const auto lazy_diff = set1 + .lazy() + .difference_with(set({2, 5, 7, 10, 15, 17})); + + EXPECT_EQ(set({1, 3, 8}), lazy_diff.get()); +} + TEST(SetTest, LazyUnionWithFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); @@ -909,6 +980,17 @@ TEST(SetTest, LazyUnionWithLazySet) EXPECT_EQ(6, map_call_count); } +TEST(SetTest, LazyUnionWithTemporaryFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + + const auto lazy_combined = set1 + .lazy() + .union_with(set({2, 5, 7, 10, 15, 17})); + + EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), lazy_combined.get()); +} + TEST(SetTest, LazyIntersectionWithFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); @@ -968,3 +1050,14 @@ TEST(SetTest, LazyIntersectionWithLazySet) EXPECT_EQ(set({3, 5, 7, 10}), intersection); EXPECT_EQ(6, map_call_count); } + +TEST(SetTest, LazyIntersectionWithTemporaryFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + + const auto lazy_intersection = set1 + .lazy() + .intersect_with(set({2, 5, 7, 10, 15, 17})); + + EXPECT_EQ(set({2, 5, 7, 10}), lazy_intersection.get()); +} From 2331c38a8e677bc4b510817893167d52c7d05f4f Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 4 May 2026 07:38:04 +0200 Subject: [PATCH 16/20] fixed release builds asserts --- include/set.h | 11 +++++++++-- include/vector.h | 31 +++++++++++++++++++++++++------ tests/vector_test.cc | 16 ++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/include/set.h b/include/set.h index 3642ef4..75ccf15 100644 --- a/include/set.h +++ b/include/set.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -376,11 +377,17 @@ namespace fcpp { const auto materialized_set = set.get(); size_t index = 0; previous([&materialized_set, &consumer, &index](const TKey& key) { - assert(index < materialized_set.size()); + if (index >= materialized_set.size()) { + assert(false); + std::abort(); + } consumer({key, materialized_set[index]}); ++index; }); - assert(index == materialized_set.size()); + if (index != materialized_set.size()) { + assert(false); + std::abort(); + } }); } diff --git a/include/vector.h b/include/vector.h index ec8b19c..42b5735 100644 --- a/include/vector.h +++ b/include/vector.h @@ -26,6 +26,7 @@ #include #include #include +#include #ifdef PARALLEL_ALGORITHM_AVAILABLE #include #endif @@ -211,11 +212,17 @@ namespace fcpp { [previous, vector_copy](const std::function&)>& consumer) { size_t index = 0; previous([&vector_copy, &consumer, &index](const T& element) { - assert(index < vector_copy.size()); + if (index >= vector_copy.size()) { + assert(false); + std::abort(); + } consumer({element, vector_copy[index]}); ++index; }); - assert(index == vector_copy.size()); + if (index != vector_copy.size()) { + assert(false); + std::abort(); + } }, capacity_hint); } @@ -233,11 +240,17 @@ namespace fcpp { [previous, vector_copy](const std::function&)>& consumer) { size_t index = 0; previous([&vector_copy, &consumer, &index](const T& element) { - assert(index < vector_copy.size()); + if (index >= vector_copy.size()) { + assert(false); + std::abort(); + } consumer({element, vector_copy[index]}); ++index; }); - assert(index == vector_copy.size()); + if (index != vector_copy.size()) { + assert(false); + std::abort(); + } }, capacity_hint); } @@ -256,11 +269,17 @@ namespace fcpp { const auto materialized_vector = vector.get(); size_t index = 0; previous([&materialized_vector, &consumer, &index](const T& element) { - assert(index < materialized_vector.size()); + if (index >= materialized_vector.size()) { + assert(false); + std::abort(); + } consumer({element, materialized_vector[index]}); ++index; }); - assert(index == materialized_vector.size()); + if (index != materialized_vector.size()) { + assert(false); + std::abort(); + } }, capacity_hint); } diff --git a/tests/vector_test.cc b/tests/vector_test.cc index cc9dcdc..9e934dc 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -1680,4 +1680,20 @@ TEST(VectorTest, LazyZipWithDifferentSizesThrows) EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); } +TEST(VectorTest, LazyZipWithFunctionalVectorDifferentSizesThrows) +{ + const vector vector_under_test({1, 2}); + const vector names({"one"}); + + EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); +} + +TEST(VectorTest, LazyZipWithLazyVectorDifferentSizesThrows) +{ + const vector vector_under_test({1, 2}); + const auto names = vector({"one"}).lazy(); + + EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); +} + #pragma warning( pop ) From a2c04070067c6b1905660ed926ab88f94b1f8053 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 4 May 2026 17:30:24 +0200 Subject: [PATCH 17/20] fixed set comparator and dead code --- include/set.h | 85 ++++++++++++++++++++++--------- tests/set_test.cc | 118 +++++++++++++++++++++++++++++++++++++++---- tests/vector_test.cc | 9 ---- 3 files changed, 171 insertions(+), 41 deletions(-) diff --git a/include/set.h b/include/set.h index 75ccf15..33f19ef 100644 --- a/include/set.h +++ b/include/set.h @@ -57,12 +57,14 @@ namespace fcpp { { public: lazy_set() - : m_operation([](const std::function&) {}) + : m_compare() + , m_operation([](const std::function&) {}) { } // Creates a lazy set by copying the provided std::set as an owned source. explicit lazy_set(const std::set& set) + : m_compare(set.key_comp()) { auto source = std::make_shared>(set); m_operation = [source](const std::function& consumer) { @@ -72,6 +74,7 @@ namespace fcpp { // Creates a lazy set by moving the provided std::set as an owned source. explicit lazy_set(std::set&& set) + : m_compare(set.key_comp()) { auto source = std::make_shared>(std::move(set)); m_operation = [source](const std::function& consumer) { @@ -82,7 +85,18 @@ namespace fcpp { // Creates a lazy set by directly providing the deferred operation. // This constructor is mostly useful for composing lazy_set instances. explicit lazy_set(std::function&)> operation) - : m_operation(std::move(operation)) + : m_compare() + , m_operation(std::move(operation)) + { + } + + // Creates a lazy set by directly providing the deferred operation and comparator. + // This constructor is mostly useful for preserving comparator state while composing + // lazy_set instances. + lazy_set(std::function&)> operation, + const TCompare& compare) + : m_compare(compare) + , m_operation(std::move(operation)) { } @@ -149,7 +163,8 @@ namespace fcpp { consumer(key); } }); - }); + }, + m_compare); } // Performs the functional `filter` algorithm lazily. @@ -179,7 +194,9 @@ namespace fcpp { // diff -> fcpp::set({1, 3, 8}) [[nodiscard]] lazy_set difference_with(const set& other) const { - return difference_with(lazy_set(std::set(other.begin(), other.end()))); + return difference_with(lazy_set(std::set(other.begin(), + other.end(), + other.key_comp()))); } // Returns the lazy set of elements which belong to this lazy set but not in the std::set. @@ -194,22 +211,25 @@ namespace fcpp { [[nodiscard]] lazy_set difference_with(const lazy_set& other) const { const auto previous = m_operation; + const auto compare = m_compare; return lazy_set( - [previous, other](const std::function& consumer) { - std::set current; + [previous, other, compare](const std::function& consumer) { + std::set current(compare); previous([¤t](const TKey& key) { current.insert(key); }); const auto materialized_other = other.get(); - std::set diff; + std::set diff(compare); std::set_difference(current.begin(), current.end(), materialized_other.begin(), materialized_other.end(), - std::inserter(diff, diff.begin())); + std::inserter(diff, diff.begin()), + compare); std::for_each(diff.begin(), diff.end(), consumer); - }); + }, + compare); } // Returns the lazy set of elements which belong either to this lazy set or the other set. @@ -227,7 +247,9 @@ namespace fcpp { // combined -> fcpp::set({1, 2, 3, 5, 7, 8, 10, 15, 17}) [[nodiscard]] lazy_set union_with(const set& other) const { - return union_with(lazy_set(std::set(other.begin(), other.end()))); + return union_with(lazy_set(std::set(other.begin(), + other.end(), + other.key_comp()))); } // Returns the lazy set of elements which belong either to this lazy set or the std::set. @@ -242,22 +264,25 @@ namespace fcpp { [[nodiscard]] lazy_set union_with(const lazy_set& other) const { const auto previous = m_operation; + const auto compare = m_compare; return lazy_set( - [previous, other](const std::function& consumer) { - std::set current; + [previous, other, compare](const std::function& consumer) { + std::set current(compare); previous([¤t](const TKey& key) { current.insert(key); }); const auto materialized_other = other.get(); - std::set combined; + std::set combined(compare); std::set_union(current.begin(), current.end(), materialized_other.begin(), materialized_other.end(), - std::inserter(combined, combined.begin())); + std::inserter(combined, combined.begin()), + compare); std::for_each(combined.begin(), combined.end(), consumer); - }); + }, + compare); } // Returns the lazy set of elements which belong to both this lazy set and the other set. @@ -275,7 +300,9 @@ namespace fcpp { // combined -> fcpp::set({2, 5, 7, 10}) [[nodiscard]] lazy_set intersect_with(const set& other) const { - return intersect_with(lazy_set(std::set(other.begin(), other.end()))); + return intersect_with(lazy_set(std::set(other.begin(), + other.end(), + other.key_comp()))); } // Returns the lazy set of elements which belong to both this lazy set and the std::set. @@ -290,22 +317,25 @@ namespace fcpp { [[nodiscard]] lazy_set intersect_with(const lazy_set& other) const { const auto previous = m_operation; + const auto compare = m_compare; return lazy_set( - [previous, other](const std::function& consumer) { - std::set current; + [previous, other, compare](const std::function& consumer) { + std::set current(compare); previous([¤t](const TKey& key) { current.insert(key); }); const auto materialized_other = other.get(); - std::set intersection; + std::set intersection(compare); std::set_intersection(current.begin(), current.end(), materialized_other.begin(), materialized_other.end(), - std::inserter(intersection, intersection.begin())); + std::inserter(intersection, intersection.begin()), + compare); std::for_each(intersection.begin(), intersection.end(), consumer); - }); + }, + compare); } // Performs the functional `zip` algorithm lazily, in which every key of the resulting @@ -314,7 +344,9 @@ namespace fcpp { template [[nodiscard]] lazy_set> zip(const set& set) const { - return zip(lazy_set(std::set(set.begin(), set.end()))); + return zip(lazy_set(std::set(set.begin(), + set.end(), + set.key_comp()))); } // Performs the functional `zip` algorithm lazily. @@ -425,6 +457,7 @@ namespace fcpp { [[nodiscard]] set get() const; private: + TCompare m_compare; std::function&)> m_operation; }; @@ -1011,6 +1044,12 @@ namespace fcpp { return m_set.size(); } + // Returns a copy of the comparator used to order this set's keys. + [[nodiscard]] TCompare key_comp() const + { + return m_set.key_comp(); + } + // Starts a lazy pipeline. The returned lazy set defers following map/filter/zip // transformations until a terminal operation, such as get() or reduce(), is called. [[nodiscard]] lazy_set lazy() const @@ -1149,7 +1188,7 @@ namespace fcpp { template [[nodiscard]] set lazy_set::get() const { - std::set materialized; + std::set materialized(m_compare); m_operation([&materialized](const TKey& key) { materialized.insert(key); }); diff --git a/tests/set_test.cc b/tests/set_test.cc index 842524e..9db8045 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -21,7 +21,6 @@ // SOFTWARE. #include -#include #include #include #include @@ -36,14 +35,6 @@ using namespace fcpp; -template -void debug(set& set) -{ - set.for_each([](const T& element) { - std::cout << element << std::endl; - }); -} - void test_contents(const set& set) { EXPECT_EQ(3, set.size()); EXPECT_EQ(1, set[0]); @@ -51,6 +42,33 @@ void test_contents(const set& set) { EXPECT_EQ(5, set[2]); } +struct stateful_descending_int_compare +{ + stateful_descending_int_compare() = delete; + + explicit stateful_descending_int_compare(bool descending) + : descending(descending) + { + } + + bool operator()(const int& lhs, const int& rhs) const + { + return descending + ? lhs > rhs + : lhs < rhs; + } + + bool descending; +}; + +std::set make_stateful_descending_set( + const std::initializer_list& values) +{ + std::set result(stateful_descending_int_compare(true)); + result.insert(values.begin(), values.end()); + return result; +} + TEST(SetTest, EmptyConstructor) { const set set_under_test; @@ -745,6 +763,22 @@ TEST(SetTest, LazySourceCanStartFromTemporaryFunctionalSet) EXPECT_EQ(set({2, 4, 6, 8}), lazy_numbers.get()); } +TEST(SetTest, LazyFilterPreservesComparatorState) +{ + const set numbers(make_stateful_descending_set({1, 2, 3, 4})); + + const auto filtered = numbers + .lazy() + .filter([](const int& value) { + return value % 2 == 0; + }) + .get(); + + EXPECT_EQ(2, filtered.size()); + EXPECT_EQ(4, filtered[0]); + EXPECT_EQ(2, filtered[1]); +} + TEST(SetTest, LazyZipWithTemporaryFunctionalSet) { const set ages({25, 45, 30, 63}); @@ -762,6 +796,24 @@ TEST(SetTest, LazyZipWithTemporaryFunctionalSet) EXPECT_EQ(expected, lazy_zipped.get()); } +TEST(SetTest, LazyZipWithFunctionalSetPreservesComparatorState) +{ + const set left({1, 2, 3}); + const set right(make_stateful_descending_set({10, 20, 30})); + + const auto zipped = left + .lazy() + .zip(right) + .get(); + + const auto expected = set>({ + std::pair(1, 30), + std::pair(2, 20), + std::pair(3, 10), + }); + EXPECT_EQ(expected, zipped); +} + TEST(SetTest, LazyZipWithTemporaryStdSet) { const set ages({25, 45, 30, 63}); @@ -920,6 +972,21 @@ TEST(SetTest, LazyDifferenceWithTemporaryFunctionalSet) EXPECT_EQ(set({1, 3, 8}), lazy_diff.get()); } +TEST(SetTest, LazyDifferenceWithFunctionalSetPreservesComparatorState) +{ + const set set1(make_stateful_descending_set({1, 2, 3, 4})); + const set set2(make_stateful_descending_set({2, 4, 6})); + + const auto diff = set1 + .lazy() + .difference_with(set2) + .get(); + + EXPECT_EQ(2, diff.size()); + EXPECT_EQ(3, diff[0]); + EXPECT_EQ(1, diff[1]); +} + TEST(SetTest, LazyUnionWithFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); @@ -991,6 +1058,24 @@ TEST(SetTest, LazyUnionWithTemporaryFunctionalSet) EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), lazy_combined.get()); } +TEST(SetTest, LazyUnionWithFunctionalSetPreservesComparatorState) +{ + const set set1(make_stateful_descending_set({1, 2, 3, 4})); + const set set2(make_stateful_descending_set({2, 4, 6})); + + const auto combined = set1 + .lazy() + .union_with(set2) + .get(); + + EXPECT_EQ(5, combined.size()); + EXPECT_EQ(6, combined[0]); + EXPECT_EQ(4, combined[1]); + EXPECT_EQ(3, combined[2]); + EXPECT_EQ(2, combined[3]); + EXPECT_EQ(1, combined[4]); +} + TEST(SetTest, LazyIntersectionWithFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); @@ -1061,3 +1146,18 @@ TEST(SetTest, LazyIntersectionWithTemporaryFunctionalSet) EXPECT_EQ(set({2, 5, 7, 10}), lazy_intersection.get()); } + +TEST(SetTest, LazyIntersectionWithFunctionalSetPreservesComparatorState) +{ + const set set1(make_stateful_descending_set({1, 2, 3, 4})); + const set set2(make_stateful_descending_set({2, 4, 6})); + + const auto intersection = set1 + .lazy() + .intersect_with(set2) + .get(); + + EXPECT_EQ(2, intersection.size()); + EXPECT_EQ(4, intersection[0]); + EXPECT_EQ(2, intersection[1]); +} diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 9e934dc..f42f3ce 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -39,14 +38,6 @@ using namespace fcpp; -template -void debug(const vector& vec) -{ - vec.for_each([](const T& element){ - std::cout << element << std::endl; - }); -} - TEST(VectorTest, InsertBack) { vector vector_under_test; From c4fca415ac1d8da57ee4f6820f3f18fb9000ba4e Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 4 May 2026 18:11:41 +0200 Subject: [PATCH 18/20] Fixed map comparator and removed dead code --- include/map.h | 24 ++++++++++++++++++++---- tests/map_test.cc | 44 ++++++++++++++++++++++++++++++++++++++++++++ tests/set_test.cc | 1 - tests/test_types.h | 6 ------ 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/include/map.h b/include/map.h index b8c804c..c0511cc 100644 --- a/include/map.h +++ b/include/map.h @@ -51,12 +51,14 @@ class lazy_map using value_type = std::pair; lazy_map() - : m_operation([](const std::function&) {}) + : m_compare() + , m_operation([](const std::function&) {}) { } // Creates a lazy map by copying the provided std::map as an owned source. explicit lazy_map(const std::map& map) + : m_compare(map.key_comp()) { auto source = std::make_shared>(map); m_operation = [source](const std::function& consumer) { @@ -66,6 +68,7 @@ class lazy_map // Creates a lazy map by moving the provided std::map as an owned source. explicit lazy_map(std::map&& map) + : m_compare(map.key_comp()) { auto source = std::make_shared>(std::move(map)); m_operation = [source](const std::function& consumer) { @@ -76,7 +79,18 @@ class lazy_map // Creates a lazy map by directly providing the deferred operation. // This constructor is mostly useful for composing lazy_map instances. explicit lazy_map(std::function&)> operation) - : m_operation(std::move(operation)) + : m_compare() + , m_operation(std::move(operation)) + { + } + + // Creates a lazy map by directly providing the deferred operation and comparator. + // This constructor is mostly useful for preserving comparator state while composing + // lazy_map instances. + lazy_map(std::function&)> operation, + const TCompare& compare) + : m_compare(compare) + , m_operation(std::move(operation)) { } @@ -146,7 +160,8 @@ class lazy_map consumer(element); } }); - }); + }, + m_compare); } // Performs the functional `filter` algorithm lazily. @@ -195,6 +210,7 @@ class lazy_map [[nodiscard]] map get() const; private: + TCompare m_compare; std::function&)> m_operation; }; @@ -689,7 +705,7 @@ class map template [[nodiscard]] map lazy_map::get() const { - std::map materialized; + std::map materialized(m_compare); m_operation([&materialized](const value_type& element) { materialized.insert(element); }); diff --git a/tests/map_test.cc b/tests/map_test.cc index 178b746..ca55941 100644 --- a/tests/map_test.cc +++ b/tests/map_test.cc @@ -38,6 +38,33 @@ void test_contents(const map& map_under_test) { EXPECT_EQ("three", map_under_test[3]); } +struct stateful_descending_int_compare +{ + stateful_descending_int_compare() = delete; + + explicit stateful_descending_int_compare(bool descending) + : descending(descending) + { + } + + bool operator()(const int& lhs, const int& rhs) const + { + return descending + ? lhs > rhs + : lhs < rhs; + } + + bool descending; +}; + +std::map make_stateful_descending_map( + const std::initializer_list>& values) +{ + std::map result(stateful_descending_int_compare(true)); + result.insert(values.begin(), values.end()); + return result; +} + TEST(MapTest, EmptyConstructor) { const map map_under_test; @@ -295,6 +322,23 @@ TEST(MapTest, LazyFiltered) EXPECT_EQ((map({{"david", 40}, {"jake", 32}, {"mary", 26}})), persons); } +TEST(MapTest, LazyFilteredPreservesComparatorState) +{ + const map numbers( + make_stateful_descending_map({{1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}})); + + const auto filtered_numbers = numbers + .lazy() + .filtered([](const std::pair& element) { + return element.first % 2 == 0; + }) + .get(); + + EXPECT_EQ((vector({4, 2})), filtered_numbers.keys()); + EXPECT_EQ("four", filtered_numbers[4]); + EXPECT_EQ("two", filtered_numbers[2]); +} + TEST(MapTest, LazyReduce) { const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); diff --git a/tests/set_test.cc b/tests/set_test.cc index 9db8045..978046e 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -311,7 +311,6 @@ TEST(SetTest, MaxCustomType) person(62, "Bob") }); const auto maximum = persons.max(); - std::cout << maximum.value().name << std::endl; #if __linux__ // NOLINT(clang-diagnostic-undef) EXPECT_EQ(person(18, "Jannet"), maximum.value()); #else diff --git a/tests/test_types.h b/tests/test_types.h index 361688f..70b66c2 100644 --- a/tests/test_types.h +++ b/tests/test_types.h @@ -45,12 +45,6 @@ struct child } }; -struct child_comparator { - bool operator() (const child& a, const child& b) const { - return a < b; - } -}; - struct person { person() From a9d429ee0cb0a3b780ad139d8d48f08ce8396192 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 4 May 2026 18:40:13 +0200 Subject: [PATCH 19/20] lazy set algebra operations normalization --- include/set.h | 15 +++++++++--- tests/set_test.cc | 61 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/include/set.h b/include/set.h index 33f19ef..fe4be3f 100644 --- a/include/set.h +++ b/include/set.h @@ -219,7 +219,7 @@ namespace fcpp { current.insert(key); }); - const auto materialized_other = other.get(); + const auto materialized_other = materialize_with_compare(other, compare); std::set diff(compare); std::set_difference(current.begin(), current.end(), @@ -272,7 +272,7 @@ namespace fcpp { current.insert(key); }); - const auto materialized_other = other.get(); + const auto materialized_other = materialize_with_compare(other, compare); std::set combined(compare); std::set_union(current.begin(), current.end(), @@ -325,7 +325,7 @@ namespace fcpp { current.insert(key); }); - const auto materialized_other = other.get(); + const auto materialized_other = materialize_with_compare(other, compare); std::set intersection(compare); std::set_intersection(current.begin(), current.end(), @@ -457,6 +457,15 @@ namespace fcpp { [[nodiscard]] set get() const; private: + [[nodiscard]] static std::set materialize_with_compare(const lazy_set& other, + const TCompare& compare) + { + const auto materialized_other = other.get(); + return std::set(materialized_other.begin(), + materialized_other.end(), + compare); + } + TCompare m_compare; std::function&)> m_operation; }; diff --git a/tests/set_test.cc b/tests/set_test.cc index 978046e..6eb8e05 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -61,14 +61,21 @@ struct stateful_descending_int_compare bool descending; }; -std::set make_stateful_descending_set( - const std::initializer_list& values) +std::set make_stateful_ordered_set( + const std::initializer_list& values, + bool descending) { - std::set result(stateful_descending_int_compare(true)); + std::set result{stateful_descending_int_compare(descending)}; result.insert(values.begin(), values.end()); return result; } +std::set make_stateful_descending_set( + const std::initializer_list& values) +{ + return make_stateful_ordered_set(values, true); +} + TEST(SetTest, EmptyConstructor) { const set set_under_test; @@ -960,6 +967,21 @@ TEST(SetTest, LazyDifferenceWithLazySet) EXPECT_EQ(6, map_call_count); } +TEST(SetTest, LazyDifferenceWithLazySetNormalizesRightHandComparatorState) +{ + const set set1(make_stateful_ordered_set({1, 2, 3, 4}, true)); + const set set2(make_stateful_ordered_set({2, 4, 6}, false)); + + const auto diff = set1 + .lazy() + .difference_with(set2.lazy()) + .get(); + + EXPECT_EQ(2, diff.size()); + EXPECT_EQ(3, diff[0]); + EXPECT_EQ(1, diff[1]); +} + TEST(SetTest, LazyDifferenceWithTemporaryFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); @@ -1046,6 +1068,24 @@ TEST(SetTest, LazyUnionWithLazySet) EXPECT_EQ(6, map_call_count); } +TEST(SetTest, LazyUnionWithLazySetNormalizesRightHandComparatorState) +{ + const set set1(make_stateful_ordered_set({1, 2, 3, 4}, true)); + const set set2(make_stateful_ordered_set({2, 4, 6}, false)); + + const auto combined = set1 + .lazy() + .union_with(set2.lazy()) + .get(); + + EXPECT_EQ(5, combined.size()); + EXPECT_EQ(6, combined[0]); + EXPECT_EQ(4, combined[1]); + EXPECT_EQ(3, combined[2]); + EXPECT_EQ(2, combined[3]); + EXPECT_EQ(1, combined[4]); +} + TEST(SetTest, LazyUnionWithTemporaryFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); @@ -1135,6 +1175,21 @@ TEST(SetTest, LazyIntersectionWithLazySet) EXPECT_EQ(6, map_call_count); } +TEST(SetTest, LazyIntersectionWithLazySetNormalizesRightHandComparatorState) +{ + const set set1(make_stateful_ordered_set({1, 2, 3, 4}, true)); + const set set2(make_stateful_ordered_set({2, 4, 6}, false)); + + const auto intersection = set1 + .lazy() + .intersect_with(set2.lazy()) + .get(); + + EXPECT_EQ(2, intersection.size()); + EXPECT_EQ(4, intersection[0]); + EXPECT_EQ(2, intersection[1]); +} + TEST(SetTest, LazyIntersectionWithTemporaryFunctionalSet) { const set set1({1, 2, 3, 5, 7, 8, 10}); From 59695cb456dde91db81dd4074adc97524fd88687 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 4 May 2026 19:07:11 +0200 Subject: [PATCH 20/20] assert for release builds --- include/set.h | 10 ++++++++-- tests/set_test.cc | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/include/set.h b/include/set.h index fe4be3f..58f6ac7 100644 --- a/include/set.h +++ b/include/set.h @@ -388,11 +388,17 @@ namespace fcpp { std::set distinct_values(materialized_vector.begin(), materialized_vector.end()); auto it = distinct_values.begin(); previous([&distinct_values, &it, &consumer](const TKey& key) { - assert(it != distinct_values.end()); + if (it == distinct_values.end()) { + assert(false); + std::abort(); + } consumer({key, *it}); ++it; }); - assert(it == distinct_values.end()); + if (it != distinct_values.end()) { + assert(false); + std::abort(); + } }); } diff --git a/tests/set_test.cc b/tests/set_test.cc index 6eb8e05..6751d9d 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -868,6 +868,22 @@ TEST(SetTest, LazyZipWithLazyVector) EXPECT_EQ(4, map_call_count); } +TEST(SetTest, LazyZipWithLazyVectorFewerDistinctValuesThrows) +{ + const set ages({25, 45}); + const vector persons({"Jake"}); + + EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons.lazy()).get(); }, ""); +} + +TEST(SetTest, LazyZipWithLazyVectorMoreDistinctValuesThrows) +{ + const set ages({25}); + const vector persons({"Jake", "Bob"}); + + EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons.lazy()).get(); }, ""); +} + TEST(SetTest, LazyZipWithLazySet) { const set ages({25, 45, 30, 63});