diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index 4c0d71a16..e3bba01e6 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -23,6 +23,7 @@ jobs: -DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF -DSOURCEMETA_CORE_LANG_PARALLEL:BOOL=OFF -DSOURCEMETA_CORE_LANG_NUMERIC:BOOL=OFF + -DSOURCEMETA_CORE_LANG_MEMORY:BOOL=OFF -DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF -DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF -DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index b753c84b7..485b295e2 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -33,6 +33,7 @@ jobs: -DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF -DSOURCEMETA_CORE_LANG_PARALLEL:BOOL=OFF -DSOURCEMETA_CORE_LANG_NUMERIC:BOOL=OFF + -DSOURCEMETA_CORE_LANG_MEMORY:BOOL=OFF -DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF -DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF -DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF diff --git a/CMakeLists.txt b/CMakeLists.txt index 2de5fe7c3..af73da73a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ option(SOURCEMETA_CORE_LANG_IO "Build the Sourcemeta Core language I/O library" option(SOURCEMETA_CORE_LANG_PROCESS "Build the Sourcemeta Core language Process library" ON) option(SOURCEMETA_CORE_LANG_PARALLEL "Build the Sourcemeta Core language parallel library" ON) option(SOURCEMETA_CORE_LANG_NUMERIC "Build the Sourcemeta Core language numeric library" ON) +option(SOURCEMETA_CORE_LANG_MEMORY "Build the Sourcemeta Core language memory library" ON) option(SOURCEMETA_CORE_LANG_ERROR "Build the Sourcemeta Core language error library" ON) option(SOURCEMETA_CORE_LANG_OPTIONS "Build the Sourcemeta Core Options library" ON) option(SOURCEMETA_CORE_LANG_TEXT "Build the Sourcemeta Core language text library" ON) @@ -116,6 +117,10 @@ if(SOURCEMETA_CORE_LANG_NUMERIC) add_subdirectory(src/lang/numeric) endif() +if(SOURCEMETA_CORE_LANG_MEMORY) + add_subdirectory(src/lang/memory) +endif() + if(SOURCEMETA_CORE_LANG_ERROR) add_subdirectory(src/lang/error) endif() @@ -299,6 +304,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/numeric) endif() + if(SOURCEMETA_CORE_LANG_MEMORY) + add_subdirectory(test/memory) + endif() + if(SOURCEMETA_CORE_LANG_ERROR) add_subdirectory(test/error) endif() diff --git a/config.cmake.in b/config.cmake.in index 642b76a4d..335d49676 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -9,6 +9,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS process) list(APPEND SOURCEMETA_CORE_COMPONENTS parallel) list(APPEND SOURCEMETA_CORE_COMPONENTS numeric) + list(APPEND SOURCEMETA_CORE_COMPONENTS memory) list(APPEND SOURCEMETA_CORE_COMPONENTS unicode) list(APPEND SOURCEMETA_CORE_COMPONENTS punycode) list(APPEND SOURCEMETA_CORE_COMPONENTS time) @@ -68,6 +69,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) elseif(component STREQUAL "numeric") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") + elseif(component STREQUAL "memory") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_memory.cmake") elseif(component STREQUAL "unicode") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") elseif(component STREQUAL "punycode") diff --git a/src/lang/memory/CMakeLists.txt b/src/lang/memory/CMakeLists.txt new file mode 100644 index 000000000..af3aaea57 --- /dev/null +++ b/src/lang/memory/CMakeLists.txt @@ -0,0 +1,6 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME memory + PRIVATE_HEADERS owned_or_reference.h) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME memory) +endif() diff --git a/src/lang/memory/include/sourcemeta/core/memory.h b/src/lang/memory/include/sourcemeta/core/memory.h new file mode 100644 index 000000000..a5faca250 --- /dev/null +++ b/src/lang/memory/include/sourcemeta/core/memory.h @@ -0,0 +1,17 @@ +#ifndef SOURCEMETA_CORE_MEMORY_H_ +#define SOURCEMETA_CORE_MEMORY_H_ + +// NOLINTBEGIN(misc-include-cleaner) +#include +// NOLINTEND(misc-include-cleaner) + +/// @defgroup memory Memory +/// @brief Growing collection of utilities for value ownership and lifetime +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#endif diff --git a/src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h b/src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h new file mode 100644 index 000000000..543afe438 --- /dev/null +++ b/src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h @@ -0,0 +1,124 @@ +#ifndef SOURCEMETA_CORE_MEMORY_OWNED_OR_REFERENCE_H_ +#define SOURCEMETA_CORE_MEMORY_OWNED_OR_REFERENCE_H_ + +#include // assert +#include // std::move_constructible, std::copy_constructible +#include // std::addressof +#include // std::optional, std::nullopt_t +#include // std::is_object_v +#include // std::move + +namespace sourcemeta::core { + +/// @ingroup memory +/// What a value that can be either owned or referred to must satisfy +template +concept Ownable = std::is_object_v && std::move_constructible; + +/// @ingroup memory +/// Either a value this holds itself, a reference to one that outlives it, or +/// nothing at all. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// static const std::string CACHED{"foo"}; +/// +/// const sourcemeta::core::OwnedOrReference reference{CACHED}; +/// assert(&reference.value() == &CACHED); +/// +/// const sourcemeta::core::OwnedOrReference owned{ +/// std::string{"bar"}}; +/// assert(owned.value() == "bar"); +/// ``` +/// +/// Reach for this when a function sometimes materialises its result and +/// sometimes hands back something it already has. Producers that build a +/// value, by reading a file, performing a network request, or computing it, +/// return it as they always would. Producers backed by storage that outlives +/// the call, such as a long lived cache, return a reference instead and skip +/// the copy. +/// +/// A reference must stay put and stay alive for as long as the consumer reads +/// it. Anything temporary binds to the owning constructor, so a temporary can +/// never be captured by reference here. A value that another one of these owns +/// does not qualify either, as assigning to that one destroys what it holds. +template class OwnedOrReference { +public: + /// Hold nothing + OwnedOrReference() = default; + + /// Hold nothing + // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) + OwnedOrReference(std::nullopt_t) {} + + /// Take ownership of a value that may or may not be there + // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) + OwnedOrReference(std::optional &&value) : owned_{std::move(value)} {} + + /// Take ownership of a value + // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) + OwnedOrReference(T &&value) : owned_{std::move(value)} {} + + /// Refer to a value that outlives this. Anything temporary binds to the + /// owning constructor above instead, so this never refers to a dead value + // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) + OwnedOrReference(const T &value) : referenced_{std::addressof(value)} {} + + // A constant temporary would otherwise bind to the referencing constructor + // and leave a dangling pointer behind + OwnedOrReference(const T &&value) = delete; + + // Prevent accidental copies, as copying is the very thing this type exists + // to avoid. Take ownership through `to_owned` instead + OwnedOrReference(const OwnedOrReference &) = delete; + auto operator=(const OwnedOrReference &) -> OwnedOrReference & = delete; + /// Move + OwnedOrReference(OwnedOrReference &&) = default; + /// Move + auto operator=(OwnedOrReference &&) -> OwnedOrReference & = default; + ~OwnedOrReference() = default; + + /// Whether there is anything to read + [[nodiscard]] auto has_value() const noexcept -> bool { + return this->referenced_ != nullptr || this->owned_.has_value(); + } + + /// Read the value, however it is held + [[nodiscard]] auto value() const -> const T & { + assert(this->has_value()); + return this->referenced_ != nullptr ? *this->referenced_ + : this->owned_.value(); + } + + /// Read the value, however it is held + [[nodiscard]] auto operator*() const -> const T & { return this->value(); } + + /// Read the value, however it is held + [[nodiscard]] auto operator->() const -> const T * { + return std::addressof(this->value()); + } + + /// Get a value the caller owns, moving out of this one when it owns it and + /// copying only when it holds a reference + [[nodiscard]] auto to_owned() && -> T + requires std::copy_constructible + { + assert(this->has_value()); + if (this->referenced_ != nullptr) { + return *this->referenced_; + } + + return std::move(this->owned_).value(); + } + +private: + std::optional owned_; + const T *referenced_{nullptr}; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/test/memory/CMakeLists.txt b/test/memory/CMakeLists.txt new file mode 100644 index 000000000..2cf03b6c9 --- /dev/null +++ b/test/memory/CMakeLists.txt @@ -0,0 +1,4 @@ +sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME memory + SOURCES memory_owned_or_reference_test.cc) + +target_link_libraries(sourcemeta_core_memory_unit PRIVATE sourcemeta::core::memory) diff --git a/test/memory/memory_owned_or_reference_test.cc b/test/memory/memory_owned_or_reference_test.cc new file mode 100644 index 000000000..d6a1e1e34 --- /dev/null +++ b/test/memory/memory_owned_or_reference_test.cc @@ -0,0 +1,218 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +static const std::string CACHED{"cached"}; + +struct Point { + int x{0}; + int y{0}; +}; + +struct HostileAddress { + int value{0}; + auto operator&() const -> const HostileAddress * { return nullptr; } +}; + +struct MoveOnly { + MoveOnly() = default; + MoveOnly(const MoveOnly &) = delete; + auto operator=(const MoveOnly &) -> MoveOnly & = delete; + MoveOnly(MoveOnly &&) = default; + auto operator=(MoveOnly &&) -> MoveOnly & = default; + ~MoveOnly() = default; +}; + +template +concept can_take_ownership = + requires(sourcemeta::core::OwnedOrReference value) { + std::move(value).to_owned(); + }; + +TEST(default_constructed_has_no_value) { + const sourcemeta::core::OwnedOrReference result; + EXPECT_FALSE(result.has_value()); +} + +TEST(nullopt_has_no_value) { + const sourcemeta::core::OwnedOrReference result{std::nullopt}; + EXPECT_FALSE(result.has_value()); +} + +TEST(empty_optional_has_no_value) { + std::optional input; + const sourcemeta::core::OwnedOrReference result{ + std::move(input)}; + EXPECT_FALSE(result.has_value()); +} + +TEST(owns_a_temporary) { + const sourcemeta::core::OwnedOrReference result{ + std::string{"owned"}}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "owned"); +} + +TEST(owns_an_optional) { + std::optional input{"owned"}; + const sourcemeta::core::OwnedOrReference result{ + std::move(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "owned"); +} + +TEST(owns_a_moved_value) { + std::string input{"owned"}; + const sourcemeta::core::OwnedOrReference result{ + std::move(input)}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "owned"); +} + +TEST(refers_to_an_external_value) { + const sourcemeta::core::OwnedOrReference result{CACHED}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "cached"); + EXPECT_EQ(&result.value(), &CACHED); +} + +TEST(refers_to_a_value_behind_a_pointer) { + const std::string *pointer{&CACHED}; + const sourcemeta::core::OwnedOrReference result{*pointer}; + EXPECT_EQ(&result.value(), &CACHED); +} + +TEST(dereference_reads_an_owned_value) { + const sourcemeta::core::OwnedOrReference result{ + std::string{"owned"}}; + EXPECT_EQ(*result, "owned"); +} + +TEST(dereference_reads_a_referenced_value) { + const sourcemeta::core::OwnedOrReference result{CACHED}; + EXPECT_EQ(&(*result), &CACHED); +} + +TEST(arrow_reaches_into_an_owned_value) { + const sourcemeta::core::OwnedOrReference result{Point{.x = 1, .y = 2}}; + EXPECT_EQ(result->x, 1); + EXPECT_EQ(result->y, 2); +} + +TEST(arrow_reaches_into_a_referenced_value) { + const sourcemeta::core::OwnedOrReference result{CACHED}; + EXPECT_EQ(result->size(), 6); +} + +TEST(to_owned_moves_out_of_an_owned_value) { + sourcemeta::core::OwnedOrReference> result{ + std::vector{"owned"}}; + const auto *const data{result.value().data()}; + const auto value{std::move(result).to_owned()}; + EXPECT_EQ(value.size(), 1); + EXPECT_EQ(value.front(), "owned"); + EXPECT_EQ(value.data(), data); +} + +TEST(to_owned_copies_a_referenced_value) { + sourcemeta::core::OwnedOrReference result{CACHED}; + const auto value{std::move(result).to_owned()}; + EXPECT_EQ(value, "cached"); + EXPECT_NE(&value, &CACHED); + EXPECT_EQ(CACHED, "cached"); +} + +TEST(move_construction_transfers_an_owned_value) { + sourcemeta::core::OwnedOrReference result{std::string{"owned"}}; + const sourcemeta::core::OwnedOrReference other{ + std::move(result)}; + EXPECT_TRUE(other.has_value()); + EXPECT_EQ(other.value(), "owned"); +} + +TEST(move_construction_transfers_a_reference) { + sourcemeta::core::OwnedOrReference result{CACHED}; + const sourcemeta::core::OwnedOrReference other{ + std::move(result)}; + EXPECT_TRUE(other.has_value()); + EXPECT_EQ(&other.value(), &CACHED); +} + +TEST(move_assignment_transfers_an_owned_value) { + sourcemeta::core::OwnedOrReference result{std::string{"owned"}}; + sourcemeta::core::OwnedOrReference other; + other = std::move(result); + EXPECT_TRUE(other.has_value()); + EXPECT_EQ(other.value(), "owned"); +} + +TEST(move_assignment_transfers_a_reference) { + sourcemeta::core::OwnedOrReference result{CACHED}; + sourcemeta::core::OwnedOrReference other; + other = std::move(result); + EXPECT_TRUE(other.has_value()); + EXPECT_EQ(&other.value(), &CACHED); +} + +TEST(cannot_be_copied) { + EXPECT_FALSE(std::is_copy_constructible_v< + sourcemeta::core::OwnedOrReference>); + EXPECT_FALSE(std::is_copy_assignable_v< + sourcemeta::core::OwnedOrReference>); +} + +TEST(can_be_moved) { + EXPECT_TRUE(std::is_move_constructible_v< + sourcemeta::core::OwnedOrReference>); + EXPECT_TRUE(std::is_move_assignable_v< + sourcemeta::core::OwnedOrReference>); +} + +TEST(cannot_refer_to_a_constant_temporary) { + EXPECT_FALSE( + (std::is_constructible_v, + const std::string &&>)); + EXPECT_TRUE( + (std::is_constructible_v, + std::string &&>)); + EXPECT_TRUE( + (std::is_constructible_v, + const std::string &>)); +} + +TEST(holds_a_move_only_value) { + const sourcemeta::core::OwnedOrReference result{MoveOnly{}}; + EXPECT_TRUE(result.has_value()); +} + +TEST(cannot_take_ownership_of_a_move_only_value) { + EXPECT_FALSE(can_take_ownership); + EXPECT_TRUE(can_take_ownership); +} + +TEST(refers_to_a_value_that_overloads_address_of) { + static const HostileAddress HOSTILE{.value = 7}; + const sourcemeta::core::OwnedOrReference result{HOSTILE}; + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().value, 7); + EXPECT_EQ(result->value, 7); + EXPECT_EQ(std::addressof(result.value()), std::addressof(HOSTILE)); +} + +TEST(ownable_accepts_object_types) { + EXPECT_TRUE(sourcemeta::core::Ownable); + EXPECT_TRUE(sourcemeta::core::Ownable); + EXPECT_TRUE(sourcemeta::core::Ownable); +} + +TEST(ownable_rejects_non_object_types) { + EXPECT_FALSE(sourcemeta::core::Ownable); + EXPECT_FALSE(sourcemeta::core::Ownable); + EXPECT_FALSE(sourcemeta::core::Ownable); +} diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index feb11daad..f682e1e7b 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(core_hello hello.cc) target_link_libraries(core_hello PRIVATE sourcemeta::core::io) target_link_libraries(core_hello PRIVATE sourcemeta::core::parallel) target_link_libraries(core_hello PRIVATE sourcemeta::core::numeric) +target_link_libraries(core_hello PRIVATE sourcemeta::core::memory) target_link_libraries(core_hello PRIVATE sourcemeta::core::error) target_link_libraries(core_hello PRIVATE sourcemeta::core::process) target_link_libraries(core_hello PRIVATE sourcemeta::core::stacktrace) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index 4f7633c6d..ffc27bdfe 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include