// Copyright (c) 2023 INRIA // All rights reserved. // // This file is part of CGAL (www.cgal.org) // // $URL$ // $Id$ // SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial // // Author(s) : Jackson Campolattaro #ifndef CGAL_PROPERTY_CONTAINTER_H #define CGAL_PROPERTY_CONTAINTER_H #include #include // todo: maybe this could be avoided #include #ifndef DOXYGEN_RUNNING namespace CGAL::Properties { template class Property_array_base { public: Property_array_base() = default; Property_array_base(const Property_array_base& rhs) = delete; virtual ~Property_array_base() = default; // Declare virtual functions here, for things which need to be done within the Property container // todo: these should mostly be private, and made available using friend virtual std::shared_ptr> empty_clone(const std::vector& active_indices) = 0; virtual std::shared_ptr> clone(const std::vector& active_indices) = 0; virtual void copy(const Property_array_base& other) = 0; virtual void move(Property_array_base&& other) = 0; virtual void append(const Property_array_base& other) = 0; virtual void reserve(std::size_t n) = 0; virtual void shrink_to_fit() = 0; virtual void swap(Index a, Index b) = 0; virtual void reset(Index i) = 0; virtual const std::type_info& type() const = 0; virtual void transfer_from(const Property_array_base& other_base, Index other_index, Index this_index) = 0; }; /*! * \brief Indexed storage for arbitrary types * * todo: make this effectively private, prioritize the use of Property_array_handle * * @tparam T */ template class Property_array : public Property_array_base { std::vector m_data; const std::vector& m_active_indices; T m_default_value; public: using value_type = T; using reference = typename std::vector::reference; using const_reference = typename std::vector::const_reference; using iterator = typename std::vector::iterator; using const_iterator = typename std::vector::const_iterator; Property_array(const std::vector& active_indices, const T& default_value) : m_data(), m_active_indices(active_indices), m_default_value(default_value) { m_data.reserve(active_indices.capacity()); m_data.resize(active_indices.size(), m_default_value); } virtual std::shared_ptr> empty_clone(const std::vector& active_indices) override { return std::make_shared>(active_indices, m_default_value); } virtual std::shared_ptr> clone(const std::vector& active_indices) override { auto new_array = std::make_shared>(active_indices, m_default_value); new_array->m_data = m_data; return new_array; } virtual void copy(const Property_array_base& other_base) override { auto& other = dynamic_cast&>(other_base); m_data = other.m_data; CGAL_precondition(m_active_indices.size() == m_data.size()); } virtual void move(Property_array_base&& other_base) override { auto&& other = static_cast&&>(other_base); m_data = std::move(other.m_data); CGAL_precondition(m_active_indices.size() == m_data.size()); } virtual void append(const Property_array_base& other_base) override { auto& other = dynamic_cast&>(other_base); CGAL_precondition(m_data.size() + other.m_data.size() == m_active_indices.size()); m_data.insert(m_data.end(), other.m_data.begin(), other.m_data.end()); } virtual void reserve(std::size_t n) override { CGAL_precondition(m_active_indices.size() == n); m_data.resize(n, m_default_value); }; virtual void shrink_to_fit() override { m_data.shrink_to_fit(); } virtual void swap(Index a, Index b) override { // todo: maybe cast to index, instead of casting index to size? CGAL_precondition(std::size_t(a) < m_data.size() && std::size_t(b) < m_data.size()); std::iter_swap(m_data.begin() + a, m_data.begin() + b); }; virtual void reset(Index i) override { CGAL_precondition(std::size_t(i) < m_data.size()); m_data[std::size_t(i)] = m_default_value; }; virtual const std::type_info& type() const override { return typeid(T); }; virtual void transfer_from(const Property_array_base& other_base, Index other_index, Index this_index) override { CGAL_precondition(other_base.type() == type()); auto& other = dynamic_cast&>(other_base); CGAL_precondition(std::size_t(other_index) < other.capacity() && std::size_t(this_index) < capacity()); m_data[this_index] = other.m_data[other_index]; } public: // todo: there's not really a good reason to use these, maybe they should be removed [[nodiscard]] std::size_t size() const { return std::count(m_active_indices.begin(), m_active_indices.end(), true); } [[nodiscard]] std::size_t capacity() const { return m_data.size(); } const_reference operator[](Index i) const { CGAL_precondition(std::size_t(i) < m_data.size()); return m_data[std::size_t(i)]; } reference operator[](Index i) { CGAL_precondition(std::size_t(i) < m_data.size()); return m_data[std::size_t(i)]; } iterator begin() { return m_data.begin(); } iterator end() { return m_data.end(); } const_iterator begin() const { return m_data.begin(); } const_iterator end() const { return m_data.end(); } public: bool operator==(const Property_array& other) const { return &other == this; } bool operator!=(const Property_array& other) const { return !operator==(other); } }; // todo: property maps/array handles should go in their own file // todo: add const/read-only handle template class Property_array_handle { std::reference_wrapper> m_array; public: // Necessary for use as a boost::property_type using key_type = Index; using value_type = T; using reference = typename std::vector::reference; using const_reference = typename std::vector::const_reference; using category = boost::lvalue_property_map_tag; using iterator = typename std::vector::iterator; using const_iterator = typename std::vector::const_iterator; Property_array_handle(Property_array& array) : m_array(array) {} [[nodiscard]] std::size_t size() const { return m_array.get().size(); } [[nodiscard]] std::size_t capacity() const { return m_array.get().capacity(); } Property_array& array() const { return m_array.get(); } // todo: This might not be needed, if the other operator[] is made const const_reference operator[](Index i) const { return m_array.get()[i]; } reference operator[](Index i) { return m_array.get()[i]; } // todo: maybe these can be const, in an lvalue property map? iterator begin() { return m_array.get().begin(); } iterator end() { return m_array.get().end(); } const_iterator begin() const { return m_array.get().begin(); } const_iterator end() const { return m_array.get().end(); } bool operator==(const Property_array_handle& other) const { return other.m_array.get() == m_array.get(); } bool operator!=(const Property_array_handle& other) const { return !operator==(other); } inline friend reference get(Property_array_handle p, const Index& i) { return p[i]; } inline friend void put(Property_array_handle p, const Index& i, const T& v) { p[i] = v; } }; template class Property_container { std::map>> m_property_arrays{}; std::vector m_active_indices{}; public: template using Array = Property_array; Property_container() = default; Property_container(const Property_container& other) { m_active_indices = other.m_active_indices; for (auto [name, array]: other.m_property_arrays) { // todo: this could probably be made faster using emplace_hint m_property_arrays.emplace( name, array->clone(m_active_indices) ); } } Property_container(Property_container&& other) { *this = std::move(other); } // todo: maybe this could be implemented in terms of the move assignment operator? Property_container& operator=(const Property_container& other) { m_active_indices = other.m_active_indices; for (auto [name, other_array]: other.m_property_arrays) { // If this container has a property by the same name auto it = m_property_arrays.find(name); if (it != m_property_arrays.end()) { auto [_, this_array] = *it; // No naming collisions with different types allowed CGAL_precondition(typeid(*this_array) == typeid(*other_array)); // Copy the data from the other array this_array->copy(*other_array); } else { // Adds the new property m_property_arrays.emplace(name, other_array->clone(m_active_indices)); } } return *this; } Property_container& operator=(Property_container&& other) { m_active_indices = std::move(other.m_active_indices); for (auto [name, other_array]: other.m_property_arrays) { // If this container has a property by the same name auto it = m_property_arrays.find(name); if (it != m_property_arrays.end()) { auto [_, this_array] = *it; // No naming collisions with different types allowed CGAL_precondition(typeid(*this_array) == typeid(*other_array)); // Copy the data from the other array this_array->copy(std::move(*other_array)); } else { // Adds the new property m_property_arrays.emplace(name, other_array->clone(m_active_indices)); } } // The moved-from property map should retain all of its properties, but contain 0 elements other.reserve(0); return *this; } template std::pair>, bool> get_or_add_property(const std::string& name, const T default_value = T()) { auto [it, created] = m_property_arrays.emplace( name, std::make_shared>( m_active_indices, default_value ) ); auto [key, array] = *it; auto& typed_array = dynamic_cast&>(*array); return {{typed_array}, created}; } template Property_array& add_property(const std::string& name, const T default_value = T()) { // todo: I'm not settled on the naming, but it's really convenient to have a function like this auto [array, created] = get_or_add_property(name, default_value); CGAL_precondition(created); return array.get(); } // todo: misleading name, maybe it could be add_same_properties? void copy_properties(const Property_container& other) { for (auto [name, other_array]: other.m_property_arrays) { // If this container doesn't have any property by this name, add it (with the same type as in other) if (!property_exists(name)) m_property_arrays.emplace(name, other_array->empty_clone(m_active_indices)); } } template const Property_array& get_property(const std::string& name) const { CGAL_precondition(m_property_arrays.count(name) != 0); return dynamic_cast&>(*m_property_arrays.at(name)); } template Property_array& get_property(const std::string& name) { CGAL_precondition(m_property_arrays.count(name) != 0); return dynamic_cast&>(*m_property_arrays.at(name)); } template std::optional>> get_property_if_exists(const std::string& name) { auto it = m_property_arrays.find(name); if (it == m_property_arrays.end()) return {}; auto [_, array] = *it; if (typeid(*array) != typeid(Property_array)) return {}; return dynamic_cast&>(*m_property_arrays.at(name)); } template bool property_exists(const std::string& name) const { auto it = m_property_arrays.find(name); if (it == m_property_arrays.end()) return false; auto [_, array] = *it; if (typeid(*array) != typeid(Property_array)) return false; return true; } // todo: maybe the non-type-strict version is useful? bool property_exists(const std::string& name) const { auto it = m_property_arrays.find(name); return (it != m_property_arrays.end()); } /*! * Removes a property array from the container * * @param name * @return True if a container with this name existed, false otherwise */ bool remove_property(const std::string& name) { return m_property_arrays.erase(name) == 1; } template bool remove_property(const Property_array& arrayToRemove) { for (auto it = m_property_arrays.begin(); it != m_property_arrays.end(); ++it) { auto const& [name, array] = *it; if (array.get() == dynamic_cast*>(&arrayToRemove)) { m_property_arrays.erase(it); return true; } } return false; } void remove_all_properties_except(const std::vector& preserved_names) { // todo: if this is used often, it should take a parameter pack instead of a vector // A fold expression could then be used in place of std::find for better performance for (auto it = m_property_arrays.begin(); it != m_property_arrays.end();) { auto const& [name, array] = *it; if (std::find(preserved_names.begin(), preserved_names.end(), name) == preserved_names.end()) it = m_property_arrays.erase(it); else it++; } } std::vector properties() const { std::vector property_names{}; for (auto const& [name, _]: m_property_arrays) property_names.emplace_back(name); return property_names; } std::size_t num_properties() const { return m_property_arrays.size(); } const std::type_info& property_type(const std::string& name) const { if (auto it = m_property_arrays.find(name); it != m_property_arrays.end()) return it->second->type(); else return typeid(void); } public: void reserve(std::size_t n) { m_active_indices.resize(n); for (auto [name, array]: m_property_arrays) array->reserve(n); } void resize(std::size_t n) { reserve(n); std::fill(m_active_indices.begin(), m_active_indices.end(), true); } [[nodiscard]] std::size_t size() const { return std::count(m_active_indices.begin(), m_active_indices.end(), true); } [[nodiscard]] std::size_t capacity() const { return m_active_indices.size(); } Index emplace_back() { // Expand the storage and return the last element reserve(capacity() + 1); m_active_indices.back() = true; auto first_new_index = Index(capacity() - 1); reset(first_new_index); return first_new_index; } Index emplace() { // If there are empty slots, return the index of one of them and mark it as full auto first_unused = std::find_if(m_active_indices.begin(), m_active_indices.end(), [](bool used) { return !used; }); if (first_unused != m_active_indices.end()) { *first_unused = true; auto index = Index(std::distance(m_active_indices.begin(), first_unused)); reset(index); return index; } return emplace_back(); } Index emplace_group_back(std::size_t n) { // Expand the storage and return the start of the new region reserve(capacity() + n); for (auto it = m_active_indices.end() - n; it < m_active_indices.end(); ++it) *it = true; return Index(capacity() - n); } Index emplace_group(std::size_t n) { auto search_start = m_active_indices.begin(); while (search_start != m_active_indices.end()) { // Find the first unused cell auto unused_begin = std::find_if( search_start, m_active_indices.end(), [](bool used) { return !used; } ); auto unused_end = unused_begin; // Determine if the group fits if (std::distance(unused_begin, m_active_indices.end()) >= n) unused_end = std::find_if( unused_begin, std::min(unused_begin + n, m_active_indices.end()), [](bool used) { return used; } ); // If the discovered range was large enough if (std::distance(unused_begin, unused_end) >= n) { // Mark the indices as used, and reset the properties of each of them // todo: it would be better to provide a function to set a range for (auto it = unused_begin; it < unused_end; ++it) { *it = true; reset(Index(std::distance(m_active_indices.begin(), it))); } // Return the first index of the range return Index(std::distance(m_active_indices.begin(), unused_begin)); } // If we didn't find a large enough region, continue our search after the end search_start = unused_end; } // If no empty regions were found, expand the storage return emplace_group_back(n); } void swap(Index a, Index b) { for (auto [name, array]: m_property_arrays) array->swap(a, b); } void reset(Index i) { for (auto [name, array]: m_property_arrays) array->reset(i); } void erase(Index i) { m_active_indices[i] = false; for (auto [name, array]: m_property_arrays) array->reset(i); } bool is_erased(Index i) const { return !m_active_indices[i]; } // todo: I'd prefer to eliminate this, if possible void mark_active(Index i) { return m_active_indices[i] = true; } void mark_inactive(Index i) { return m_active_indices[i] = false; } std::vector active_list() const { std::vector indices; for (std::size_t i = 0; i < m_active_indices.size(); ++i) if (m_active_indices[i]) indices.emplace_back(i); return indices; } std::vector inactive_list() const { std::vector indices; for (std::size_t i = 0; i < m_active_indices.size(); ++i) if (!m_active_indices[i]) indices.emplace_back(i); return indices; } void shrink_to_fit() { for (auto [name, array]: m_property_arrays) array->shrink_to_fit(); } /*! * Adds the elements of the other container to this container for each property which is present in this container. * * Gaps in both containers are preserved, and all elements of the other container are guaranteed * to appear after the elements of this container. * Properties in this container which don't appear in the other container are extended with default values. * Properties in the other container which don't appear in this one are not included. * todo: merge() would be useful as well, but could break contiguous regions in the other container * * @param other */ void append(const Property_container& other) { m_active_indices.insert(m_active_indices.end(), other.m_active_indices.begin(), other.m_active_indices.end()); for (auto [name, array]: m_property_arrays) { auto it = other.m_property_arrays.find(name); if (it != other.m_property_arrays.end()) array->append(*it->second); else array->reserve(m_active_indices.size()); } } // todo: maybe should be renamed to transfer_from, but I'd rather remove this functionality entirely void transfer(const Property_container& other, Index other_index, Index this_index) { CGAL_precondition(other.m_property_arrays.size() == m_property_arrays.size()); for (auto [name, array]: m_property_arrays) { auto other_array = other.m_property_arrays.at(name); array->transfer_from(*other_array, other_index, this_index); } } // todo: maybe a compress() method? }; } #endif // DOXYGEN_RUNNING #endif //CGAL_PROPERTY_CONTAINTER_H