diff --git a/immer/extra/archive/box/archive.hpp b/immer/extra/archive/box/archive.hpp index 3e387376..df290500 100644 --- a/immer/extra/archive/box/archive.hpp +++ b/immer/extra/archive/box/archive.hpp @@ -10,6 +10,8 @@ #include +#include + namespace immer::archive::box { template @@ -115,25 +117,48 @@ save_to_archive(immer::box box, return {std::move(archive), id}; } -template +template , + typename TransformF = boost::hana::id_t> class loader { public: - explicit loader(archive_load ar) + explicit loader(Archive ar) + requires std::is_same_v + : ar_{std::move(ar)} + { + } + + explicit loader(Archive ar, TransformF transform) : ar_{std::move(ar)} + , transform_{std::move(transform)} { } - immer::box load(container_id id) const + immer::box load(container_id id) { if (id.value >= ar_.boxes.size()) { throw invalid_container_id{id}; } - return ar_.boxes[id.value]; + if constexpr (std::is_same_v) { + return ar_.boxes[id.value]; + } else { + if (auto* b = boxes.find(id)) { + return *b; + } + const auto& old_box = ar_.boxes[id.value]; + auto new_box = + immer::box{transform_(old_box.get())}; + boxes = std::move(boxes).set(id, new_box); + return new_box; + } } private: - const archive_load ar_; + const Archive ar_; + const TransformF transform_; + immer::map> boxes; }; template @@ -166,8 +191,12 @@ struct container_traits> { using save_archive_t = box::archive_save; using load_archive_t = box::archive_load; - using loader_t = box::loader; - using container_id = immer::archive::container_id; + + template + using loader_t = box::loader; + + using container_id = immer::archive::container_id; template static auto transform(F&& func) diff --git a/immer/extra/archive/cereal/immer_box.hpp b/immer/extra/archive/cereal/immer_box.hpp new file mode 100644 index 00000000..438da399 --- /dev/null +++ b/immer/extra/archive/cereal/immer_box.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace cereal { + +template +void CEREAL_LOAD_FUNCTION_NAME(Archive& ar, immer::box& m) +{ + T x; + ar(x); + m = x; +} + +template +void CEREAL_SAVE_FUNCTION_NAME(Archive& ar, + const immer::box& m) +{ + ar(m.get()); +} + +} // namespace cereal diff --git a/immer/extra/archive/champ/champ.hpp b/immer/extra/archive/champ/champ.hpp index ed02e1d2..d6e81933 100644 --- a/immer/extra/archive/champ/champ.hpp +++ b/immer/extra/archive/champ/champ.hpp @@ -44,7 +44,9 @@ class hash_validation_failed_exception : public archive_exception } }; -template +template , + typename TransformF = boost::hana::id_t> class container_loader { using champ_t = std::decay_t().impl())>; @@ -61,12 +63,19 @@ class container_loader }; public: - explicit container_loader(container_archive_load archive) + explicit container_loader(Archive archive) + requires std::is_same_v : archive_{std::move(archive)} , nodes_{archive_.nodes} { } + explicit container_loader(Archive archive, TransformF transform) + : archive_{std::move(archive)} + , nodes_{archive_.nodes, std::move(transform)} + { + } + Container load(node_id root_id) { if (root_id.value >= archive_.nodes.size()) { @@ -106,15 +115,20 @@ class container_loader } private: - const container_archive_load archive_; + const Archive archive_; nodes_loader + traits::bits, + TransformF> nodes_; }; +template +container_loader(container_archive_load archive) + -> container_loader; + template std::pair, node_id> save_to_archive(Container container, container_archive_save archive) diff --git a/immer/extra/archive/champ/load.hpp b/immer/extra/archive/champ/load.hpp index 391bb32d..ed6909b4 100644 --- a/immer/extra/archive/champ/load.hpp +++ b/immer/extra/archive/champ/load.hpp @@ -6,7 +6,9 @@ #include +#include #include + #include namespace immer::archive { @@ -54,7 +56,8 @@ template , typename Equal = std::equal_to, typename MemoryPolicy = immer::default_memory_policy, - immer::detail::hamts::bits_t B = immer::default_bits> + immer::detail::hamts::bits_t B = immer::default_bits, + typename TransformF = boost::hana::id_t> class nodes_loader { public: @@ -66,7 +69,14 @@ class nodes_loader using values_t = immer::flex_vector>; explicit nodes_loader(nodes_load archive) + requires std::is_same_v + : archive_{std::move(archive)} + { + } + + explicit nodes_loader(nodes_load archive, TransformF transform) : archive_{std::move(archive)} + , transform_{std::move(transform)} { } @@ -219,6 +229,7 @@ class nodes_loader private: const nodes_load archive_; + const TransformF transform_; immer::map> collisions_; immer::map> inners_; }; diff --git a/immer/extra/archive/champ/traits.hpp b/immer/extra/archive/champ/traits.hpp index 0f4b24a4..295ea396 100644 --- a/immer/extra/archive/champ/traits.hpp +++ b/immer/extra/archive/champ/traits.hpp @@ -4,6 +4,8 @@ #include #include +#include + namespace immer::archive { template @@ -13,8 +15,12 @@ struct champ_traits immer::archive::champ::container_archive_save; using load_archive_t = immer::archive::champ::container_archive_load; - using loader_t = immer::archive::champ::container_loader; using container_id = immer::archive::node_id; + + template + using loader_t = + immer::archive::champ::container_loader; }; template +class QQ; + namespace hana = boost::hana; /** @@ -85,32 +88,63 @@ struct archives_save } }; -template +template ::load_archive_t, + class TransformF = boost::hana::id_t, + class OldContainerType = boost::hana::id_t> struct archive_type_load { - using archive_t = typename container_traits::load_archive_t; + using container_t = Container; + using old_container_t = OldContainerType; - archive_t archive = {}; - std::optional::loader_t> loader; + Archive archive = {}; + TransformF transform; + std::optional::template loader_t> + loader; archive_type_load() = default; - explicit archive_type_load(archive_t archive_) + explicit archive_type_load(Archive archive_) + requires std::is_same_v : archive{std::move(archive_)} { } + explicit archive_type_load(Archive archive_, TransformF transform_) + : archive{std::move(archive_)} + , transform{std::move(transform_)} + { + } + archive_type_load(const archive_type_load& other) : archive{other.archive} + , transform{other.transform} { } archive_type_load& operator=(const archive_type_load& other) { - archive = other.archive; + archive = other.archive; + transform = other.transform; return *this; } + template + auto with_transform(Func&& func) const + { + using value_type = typename Container::value_type; + using new_value_type = + std::decay_t()))>; + using NewContainer = + std::decay_t::transform( + func))>; + using TransF = std::function; + // the transform function must be filled in later + return archive_type_load{ + archive, TransF{}}; + } + friend bool operator==(const archive_type_load& left, const archive_type_load& right) { @@ -157,7 +191,29 @@ struct archives_load auto& load = storage[hana::type_c]; if (!load.loader) { - load.loader.emplace(load.archive); + load.loader.emplace(load.archive, load.transform); + } + return *load.loader; + } + + template + auto& get_loader_by_old_container() + { + constexpr auto find_key = [](const auto& storage) { + return hana::find_if( + hana::keys(storage), + [&](auto key) { + using type1 = typename std::decay_t< + decltype(storage[key])>::old_container_t; + return hana::type_c == + hana::type_c; + }) + .value(); + }; + using Key = decltype(find_key(storage)); + auto& load = storage[Key{}]; + if (!load.loader) { + load.loader.emplace(load.archive, load.transform); } return *load.loader; } @@ -247,36 +303,28 @@ struct archives_load // Return a pair where second is an optional. Optional is already // populated if no transformation is required. - const auto transform_pair_initial = [fake_convert_container, - &conversion_map]( - const auto& pair) { - using Contains = - decltype(hana::contains(conversion_map, hana::first(pair))); - constexpr bool contains = hana::value(); - if constexpr (contains) { - // Look up the conversion function by the type from the original - // archive. - const auto& func = inject_argument( - fake_convert_container, conversion_map[hana::first(pair)]); - - using Container = typename decltype(+hana::first(pair))::type; - using NewContainer = std::decay_t< - decltype(container_traits::transform(func))>; - const auto old_key = hana::first(pair); - return hana::make_pair( - hana::type_c, - hana::make_tuple( - old_key, - std::optional>{})); - } else { - // If the conversion map doesn't mention the current type, we - // leave it as is. - return hana::make_pair( - hana::first(pair), - hana::make_tuple(hana::first(pair), - std::make_optional(hana::second(pair)))); - } - }; + const auto transform_pair_initial = + [fake_convert_container, &conversion_map, this](const auto& pair) { + using Contains = + decltype(hana::contains(conversion_map, hana::first(pair))); + constexpr bool contains = hana::value(); + if constexpr (contains) { + // Look up the conversion function by the type from the + // original archive. + const auto& func = + inject_argument(fake_convert_container, + conversion_map[hana::first(pair)]); + auto type_load = + storage[hana::first(pair)].with_transform(func); + using NewContainer = decltype(type_load)::container_t; + return hana::make_pair(hana::type_c, + std::move(type_load)); + } else { + // If the conversion map doesn't mention the current type, + // we leave it as is. + return pair; + } + }; // Each archive is wrapped in optional to know if it's already been // converted or not, since the order is unknown and depends on the @@ -285,76 +333,54 @@ struct archives_load // This temporary storage is a map from type to // (old_key, optional). Some optionals are already // populated, if no transformation is required. - auto optional_storage = hana::fold_left( + auto optional_storage_ = hana::fold_left( storage, hana::make_map(), [transform_pair_initial](auto map, auto pair) { return hana::insert(map, transform_pair_initial(pair)); }); - const auto process = - [this](auto old_key, auto& optional_archive, const auto& func) { - if (optional_archive) { - // The archive has already been processed, do nothing - return; - } - const auto& archive = storage[old_key].archive; - // Each archive defines the transform_archive function that - // transforms its leaves with the given function. - optional_archive.emplace(transform_archive(archive, func)); - }; + auto optional_storage = + std::make_shared>( + optional_storage_); // The get_loader function accepts a new container type as an argument // (after transformation, if any) and returns a reference to a loader // that is able to load such container types. - const auto get_loader = [&optional_storage, &conversion_map, process]( - const auto& convert_container, - auto new_container_type) -> auto& { - auto& s = optional_storage[new_container_type]; - auto& st = hana::at_c<1>(s); - const auto old_key = hana::at_c<0>(s); - const auto& old_func = conversion_map[old_key]; - auto func = inject_argument(convert_container, old_func); - process(old_key, st, func); - - auto& type_load = *st; + const auto get_loader = + [optional_storage](auto new_container_type) -> auto& { + auto& type_load = (*optional_storage)[new_container_type]; if (!type_load.loader) { - type_load.loader.emplace(type_load.archive); + type_load.loader.emplace(type_load.archive, + type_load.transform); } return *type_load.loader; }; const auto convert_container = - hana::fix([&get_id, get_loader]( - auto self, auto new_type, const auto& old_container) { + [get_id, get_loader](auto new_type, const auto& old_container) { const auto id = get_id(old_container); - auto& loader = get_loader(self, new_type); + auto& loader = get_loader(new_type); return loader.load(id); - }); + }; // Call `process` for each type - hana::for_each(hana::keys(optional_storage), [&](auto key) { - auto& s = optional_storage[key]; - auto& st = hana::at_c<1>(s); - const auto old_key = hana::at_c<0>(s); + hana::for_each(hana::keys(*optional_storage), [&](auto key) { + auto& st = (*optional_storage)[key]; + using TypeLoad = std::decay_t; + const auto old_key = + hana::type_c; constexpr auto needs_conversion = hana::value(); if constexpr (needs_conversion) { const auto& old_func = conversion_map[old_key]; - auto func = inject_argument(convert_container, old_func); - process(old_key, st, func); + auto func = inject_argument(convert_container, old_func); + st.transform = std::move(func); } }); // By now, all optionals have been emplaced. - auto new_storage = hana::fold_left( - optional_storage, hana::make_map(), [](auto map, auto pair) { - // Extract from std::optional - return hana::insert( - map, - hana::make_pair(hana::first(pair), - hana::at_c<1>(hana::second(pair)).value())); - }); + auto new_storage = *optional_storage; using NewStorage = decltype(new_storage); return archives_load{std::move(new_storage)}; } @@ -589,6 +615,20 @@ T from_json_with_archive_with_conversion(const std::string& input, return from_json_with_archive_with_conversion(is, map); } +template +auto get_container_id(const detail::archives_save& archives, + const Container& container) +{ + const auto& old_archive = + archives.template get_save_archive>(); + const auto [new_archive, id] = save_to_archive(container, old_archive); + if (!(new_archive == old_archive)) { + throw std::logic_error{ + "Expecting that the container has already been archived"}; + } + return id; +} + /** * Given an archives_save and a map of transformations, produce a new type of * load archive with those transformations applied @@ -599,19 +639,35 @@ inline auto transform_save_archive( const ConversionMap& conversion_map) { const auto old_load_archives = to_load_archives(old_archives); - const auto get_id = [&old_archives](const auto& immer_container) { - using Container = std::decay_t; - const auto& old_archive = - old_archives.template get_save_archive(); - const auto [new_archive, id] = - save_to_archive(immer_container, old_archive); - if (!(new_archive == old_archive)) { - throw std::logic_error{ - "Expecting that the container has already been archived"}; - } - return id; + // NOTE: We have to copy old_archives here because the get_id function will + // be called later, as the conversion process is lazy. + const auto get_id = [old_archives](const auto& immer_container) { + return get_container_id(old_archives, immer_container); }; return old_load_archives.transform_recursive(conversion_map, get_id); } +/** + * Given an old save archives and a new (transformed) load archives, effectively + * convert the given container. + */ +template +auto convert_container( + const detail::archives_save& old_save_archives, + detail::archives_load& new_load_archives, + const Container& container) +{ + const auto container_id = get_container_id(old_save_archives, container); + auto& loader = + new_load_archives + .template get_loader_by_old_container>(); + auto result = loader.load(container_id); + // return std::make_pair(std::move(result), std::move(new_load_archives)); + return result; +} + } // namespace immer::archive diff --git a/immer/extra/archive/rbts/load.hpp b/immer/extra/archive/rbts/load.hpp index e9da42fe..ad48ce5a 100644 --- a/immer/extra/archive/rbts/load.hpp +++ b/immer/extra/archive/rbts/load.hpp @@ -81,7 +81,9 @@ class same_depth_children_exception : public archive_exception template + immer::detail::rbts::bits_t BL, + class Archive = archive_load, + class TransformF = boost::hana::id_t> class loader { public: @@ -91,8 +93,15 @@ class loader using node_ptr = node_ptr; using nodes_set_t = immer::set; - explicit loader(archive_load ar) + explicit loader(Archive ar) + requires std::is_same_v + : ar_{std::move(ar)} + { + } + + explicit loader(Archive ar, TransformF transform) : ar_{std::move(ar)} + , transform_{std::move(transform)} { } @@ -169,8 +178,12 @@ class loader auto leaf = node_ptr{n ? node_t::make_leaf_n(n) : rbtree::empty_tail(), [n](auto* ptr) { node_t::delete_leaf(ptr, n); }}; + auto values = std::vector{}; + for (const auto& item : node_info->data) { + values.push_back(transform_(item)); + } immer::detail::uninitialized_copy( - node_info->data.begin(), node_info->data.end(), leaf.get()->leaf()); + values.begin(), values.end(), leaf.get()->leaf()); leaves_ = std::move(leaves_).set(id, leaf); loaded_leaves_ = std::move(loaded_leaves_).set(leaf.get(), id); return leaf; @@ -462,7 +475,8 @@ class loader } private: - const archive_load ar_; + const Archive ar_; + const TransformF transform_; immer::map leaves_; immer::map inners_; immer::map loaded_leaves_; @@ -474,19 +488,27 @@ class loader template + immer::detail::rbts::bits_t BL, + class Archive = archive_load, + class TransformF = boost::hana::id_t> class vector_loader { public: - explicit vector_loader(archive_load ar) + explicit vector_loader(Archive ar) + requires std::is_same_v : loader{std::move(ar)} { } + explicit vector_loader(Archive ar, TransformF transform) + : loader{std::move(ar), std::move(transform)} + { + } + auto load(container_id id) { return loader.load_vector(id); } private: - loader loader; + loader loader; }; template &, template + immer::detail::rbts::bits_t BL, + class Archive = archive_load, + class TransformF = boost::hana::id_t> class flex_vector_loader { public: - explicit flex_vector_loader(archive_load ar) - : loader{std::move(ar)} + explicit flex_vector_loader(Archive ar, TransformF transform = {}) + : loader{std::move(ar), std::move(transform)} { } auto load(container_id id) { return loader.load_flex_vector(id); } private: - loader loader; + loader loader; }; template > { using save_archive_t = rbts::archive_save; using load_archive_t = rbts::archive_load; - using loader_t = rbts::vector_loader; using container_id = immer::archive::container_id; + template + using loader_t = + rbts::vector_loader; + // This function is used to determine the type of the container after // applying some transformation. template @@ -35,9 +39,12 @@ struct container_traits> { using save_archive_t = rbts::archive_save; using load_archive_t = rbts::archive_load; - using loader_t = rbts::flex_vector_loader; using container_id = immer::archive::container_id; + template + using loader_t = rbts::flex_vector_loader; + template static auto transform(F&& func) { diff --git a/test/extra/archive/test_circular_dependency_conversion.cpp b/test/extra/archive/test_circular_dependency_conversion.cpp index 0034ee44..682e5a84 100644 --- a/test/extra/archive/test_circular_dependency_conversion.cpp +++ b/test/extra/archive/test_circular_dependency_conversion.cpp @@ -4,6 +4,8 @@ #include "utils.hpp" +#include + #define DEFINE_OPERATIONS(name) \ bool operator==(const name& left, const name& right) \ { \ @@ -32,8 +34,9 @@ struct two_boxed { BOOST_HANA_DEFINE_STRUCT(two_boxed, (immer::box, two)); - two_boxed() = default; - two_boxed(value_two val); + two_boxed() = default; + two_boxed(const two_boxed&) = default; + explicit two_boxed(value_two val); }; struct value_one @@ -47,6 +50,7 @@ struct value_one struct value_two { + int number = {}; vector_one ones; }; @@ -57,7 +61,7 @@ two_boxed::two_boxed(value_two val) } // namespace model -BOOST_HANA_ADAPT_STRUCT(model::value_two, ones); +BOOST_HANA_ADAPT_STRUCT(model::value_two, number, ones); namespace model { DEFINE_OPERATIONS(two_boxed); @@ -73,8 +77,9 @@ struct two_boxed { BOOST_HANA_DEFINE_STRUCT(two_boxed, (immer::box, two)); - two_boxed() = default; - two_boxed(value_two val); + two_boxed() = default; + two_boxed(const two_boxed&) = default; + explicit two_boxed(immer::box two_); }; struct value_one @@ -88,17 +93,18 @@ struct value_one struct value_two { + int number = {}; vector_one ones; }; -two_boxed::two_boxed(value_two val) - : two{val} +two_boxed::two_boxed(immer::box two_) + : two{std::move(two_)} { } } // namespace format -BOOST_HANA_ADAPT_STRUCT(format::value_two, ones); +BOOST_HANA_ADAPT_STRUCT(format::value_two, number, ones); namespace format { DEFINE_OPERATIONS(two_boxed); @@ -106,10 +112,13 @@ DEFINE_OPERATIONS(value_one); DEFINE_OPERATIONS(value_two); } // namespace format -TEST_CASE("Test circular dependency archives", "[.broken]") +TEST_CASE("Test circular dependency archives") { - const auto two1 = model::two_boxed{}; + const auto two1 = model::two_boxed{model::value_two{ + .number = 456, + }}; const auto two2 = model::two_boxed{model::value_two{ + .number = 123, .ones = { model::value_one{ @@ -123,9 +132,7 @@ TEST_CASE("Test circular dependency archives", "[.broken]") const auto names = immer::archive::get_archives_for_types( hana::tuple_t, - hana::make_map( - - )); + hana::make_map()); const auto [json_str, model_archives] = immer::archive::to_json_with_auto_archive(value, names); // REQUIRE(json_str == ""); @@ -139,8 +146,9 @@ TEST_CASE("Test circular dependency archives", "[.broken]") hana::type_c>, [](model::two_boxed old, const auto& convert_container) { SPDLOG_INFO("converting model::two_boxed"); - return format::two_boxed{convert_container( - hana::type_c>, old.two)}; + const auto new_box = convert_container( + hana::type_c>, old.two); + return format::two_boxed{new_box}; }), hana::make_pair( hana::type_c>, @@ -148,7 +156,10 @@ TEST_CASE("Test circular dependency archives", "[.broken]") SPDLOG_INFO("converting model::value_two"); auto ones = convert_container( hana::type_c>, old.ones); - return format::value_two{ones}; + return format::value_two{ + .number = old.number, + .ones = ones, + }; }), hana::make_pair( hana::type_c>, @@ -160,7 +171,40 @@ TEST_CASE("Test circular dependency archives", "[.broken]") }) ); - const auto format_load_archives = + auto format_load_archives = immer::archive::transform_save_archive(model_archives, map); (void) format_load_archives; + + const auto format_twos = immer::archive::convert_container( + model_archives, format_load_archives, value.twos); + + SECTION("Same thing twice, same result") + { + const auto format_twos_2 = immer::archive::convert_container( + model_archives, format_load_archives, value.twos); + REQUIRE(format_twos.identity() == format_twos_2.identity()); + } + + // Confirm there is internal sharing happening + REQUIRE(value.twos[0].two == value.twos[1].two.get().ones[0].twos[0].two); + REQUIRE(value.twos[0].two.impl() == + value.twos[1].two.get().ones[0].twos[0].two.impl()); + + REQUIRE(value.twos[0].two.get().number == format_twos[0].two.get().number); + + REQUIRE(format_twos[0].two == format_twos[1].two.get().ones[0].twos[0].two); + REQUIRE(format_twos[0].two.impl() == + format_twos[1].two.get().ones[0].twos[0].two.impl()); + + { + const auto format_names = immer::archive::get_archives_for_types( + hana::tuple_t, + hana::make_map()); + const auto [loaded_json_str, model_archives] = + immer::archive::to_json_with_auto_archive(format_twos, + format_names); + REQUIRE(test::to_json(value.twos) == test::to_json(format_twos)); + } }