diff --git a/include/iris/x4/attribute/smart_ptr.hpp b/include/iris/x4/attribute/smart_ptr.hpp new file mode 100644 index 000000000..4cd6ed234 --- /dev/null +++ b/include/iris/x4/attribute/smart_ptr.hpp @@ -0,0 +1,384 @@ +#ifndef IRIS_ZZ_X4_ATTRIBUTE_SMART_PTR_HPP +#define IRIS_ZZ_X4_ATTRIBUTE_SMART_PTR_HPP + +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + Distributed under the Boost Software License, Version 1.0. (See accompanying + file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +==============================================================================*/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace iris::x4 { + +namespace detail { + +// Since we need to do double-buffering anyway, we can additionally +// provide strong exception guarantee in the parse(...) functions +// below. +template +struct [[nodiscard]] smart_ptr_rollback_guard +{ + ~smart_ptr_rollback_guard() noexcept + { + if (!parse_ok) { + ptr = std::move(old_ptr); + } + } + + bool& parse_ok; + Ptr& old_ptr; + Ptr& ptr; +}; + +// ---------------------------------------------------- + +template +struct unique_ptr_parser_base + : proxy_parser +{ + using base_type = proxy_parser; + + // https://eel.is/c++draft/unique.ptr.single.ctor + + template + requires + (!std::is_pointer_v && std::is_default_constructible_v) && + (!std::same_as, Derived>) && + std::is_constructible_v + constexpr explicit unique_ptr_parser_base(SubjectT&& subject) + noexcept( + std::is_nothrow_constructible_v && + std::is_nothrow_default_constructible_v + ) + : base_type(std::forward(subject)) + {} + + template + requires + (!std::same_as, Derived>) && + std::is_constructible_v && + std::is_constructible_v + constexpr unique_ptr_parser_base(SubjectT&& subject, DeleterT const& d) + noexcept( + std::is_nothrow_constructible_v && + std::is_nothrow_default_constructible_v + ) + : base_type(std::forward(subject)) + , deleter_(d) + {} + + template + requires + (!std::same_as, Derived>) && + std::is_constructible_v && + std::is_constructible_v + constexpr unique_ptr_parser_base(SubjectT&& subject, std::remove_reference_t&& d) + noexcept( + std::is_nothrow_constructible_v && + std::is_nothrow_default_constructible_v + ) + : base_type(std::forward(subject)) + , deleter_(std::move(d)) + {} + + template Se, class Context, X4Attribute Attr> + [[nodiscard]] constexpr bool + parse(It& it, Se const& se, Context const& ctx, Attr& ptr) const + noexcept(false) // never noexcept; requires dynamic memory allocation + { + static_assert(std::same_as, "Incompatible deleter type provided for unique_ptr_parser"); + + bool parse_ok = false; + auto old_ptr = std::exchange(ptr, std::unique_ptr(new T(), deleter_)); + + smart_ptr_rollback_guard> + guard{parse_ok, old_ptr, ptr}; + + parse_ok = this->subject.parse(it, se, ctx, *ptr); + return parse_ok; + } + +private: + IRIS_NO_UNIQUE_ADDRESS DeleterT deleter_{}; +}; + +template +struct unique_ptr_parser_base> + : proxy_parser +{ + using base_type = proxy_parser; + + template + requires + (!std::same_as, Derived>) && + std::is_constructible_v + constexpr explicit unique_ptr_parser_base(SubjectT&& subject) + noexcept(std::is_nothrow_constructible_v) + : base_type(std::forward(subject)) + {} + + template Se, class Context, X4Attribute Attr> + [[nodiscard]] constexpr bool + parse(It& it, Se const& se, Context const& ctx, Attr& ptr) const + noexcept(false) // never noexcept; requires dynamic memory allocation + { + static_assert(std::same_as>, "Incompatible deleter type provided for unique_ptr_parser"); + + bool parse_ok = false; + auto old_ptr = std::exchange(ptr, std::make_unique()); + + smart_ptr_rollback_guard>> + guard{parse_ok, old_ptr, ptr}; + + parse_ok = this->subject.parse(it, se, ctx, *ptr); + return parse_ok; + } +}; + +} // detail + +template::attribute_type, class DeleterT = std::default_delete> +struct unique_ptr_parser : detail::unique_ptr_parser_base< + unique_ptr_parser, + Subject, T, DeleterT +> +{ + static_assert(X4Attribute); + static_assert(!X4UnusedAttribute, "*_ptr_parser with `unused_type` is meaningless"); + + using element_type = T; + using deleter_type = DeleterT; + using attribute_type = std::unique_ptr; + + static constexpr bool requires_exact_attribute_type = true; + + using unique_ptr_parser::unique_ptr_parser_base::unique_ptr_parser_base; + + [[nodiscard]] constexpr std::string get_x4_info() const + { + return std::format("unique_ptr({})", get_info{}(this->subject)); + } +}; + +namespace parsers { + +template +[[nodiscard]] constexpr auto unique_ptr(Subject&& subject) + noexcept( + is_parser_nothrow_castable_v && + std::is_nothrow_constructible_v< + unique_ptr_parser>, + as_parser_t + > + ) +{ + return unique_ptr_parser>{ + as_parser(std::forward(subject)) + }; +} + +template, X4Subject Subject> +[[nodiscard]] constexpr auto unique_ptr(Subject&& subject) + noexcept( + is_parser_nothrow_castable_v && + std::is_nothrow_constructible_v< + unique_ptr_parser, T, DeleterT>, + as_parser_t + > + ) +{ + return unique_ptr_parser, T, DeleterT>{ + as_parser(std::forward(subject)) + }; +} + +// ^^^ These cannot be CPO because we want to overload them via template parameter + +} // parsers + +using parsers::unique_ptr; + + +// ---------------------------------------------------- +// ---------------------------------------------------- +// ---------------------------------------------------- + + +namespace detail { + +template +struct shared_ptr_parser_base + : proxy_parser +{ + using base_type = proxy_parser; + + // https://eel.is/c++draft/util.smartptr.shared.const + + template + requires + (!std::is_pointer_v && std::is_default_constructible_v) && + (!std::same_as, Derived>) && + std::is_constructible_v + constexpr explicit shared_ptr_parser_base(SubjectT&& subject) + noexcept( + std::is_nothrow_constructible_v && + std::is_nothrow_default_constructible_v + ) + : base_type(std::forward(subject)) + {} + + template + requires + (!std::same_as, Derived>) && + std::is_constructible_v && + std::is_constructible_v + constexpr shared_ptr_parser_base(SubjectT&& subject, DeleterT const& d) + noexcept( + std::is_nothrow_constructible_v && + std::is_nothrow_default_constructible_v + ) + : base_type(std::forward(subject)) + , deleter_(d) + {} + + template + requires + (!std::same_as, Derived>) && + std::is_constructible_v && + std::is_constructible_v + constexpr shared_ptr_parser_base(SubjectT&& subject, std::remove_reference_t&& d) + noexcept( + std::is_nothrow_constructible_v && + std::is_nothrow_default_constructible_v + ) + : base_type(std::forward(subject)) + , deleter_(std::move(d)) + {} + + template Se, class Context, X4Attribute Attr> + [[nodiscard]] constexpr bool + parse(It& it, Se const& se, Context const& ctx, Attr& ptr) const + noexcept(false) // never noexcept; requires dynamic memory allocation + { + bool parse_ok = false; + auto old_ptr = std::exchange(ptr, std::shared_ptr(new T(), deleter_)); + + smart_ptr_rollback_guard> + guard{parse_ok, old_ptr, ptr}; + + parse_ok = this->subject.parse(it, se, ctx, *ptr); + return parse_ok; + } + +private: + IRIS_NO_UNIQUE_ADDRESS DeleterT deleter_{}; +}; + +template +struct shared_ptr_parser_base> + : proxy_parser +{ + using base_type = proxy_parser; + + template + requires + (!std::same_as, Derived>) && + std::is_constructible_v + constexpr explicit shared_ptr_parser_base(SubjectT&& subject) + noexcept(std::is_nothrow_constructible_v) + : base_type(std::forward(subject)) + {} + + template Se, class Context, X4Attribute Attr> + [[nodiscard]] constexpr bool + parse(It& it, Se const& se, Context const& ctx, Attr& ptr) const + noexcept(false) // never noexcept; requires dynamic memory allocation + { + bool parse_ok = false; + auto old_ptr = std::exchange(ptr, std::make_shared()); + + smart_ptr_rollback_guard> + guard{parse_ok, old_ptr, ptr}; + + parse_ok = this->subject.parse(it, se, ctx, *ptr); + return parse_ok; + } +}; + +} // detail + +template::attribute_type, class DeleterT = std::default_delete> +struct shared_ptr_parser : detail::shared_ptr_parser_base< + shared_ptr_parser, + Subject, T, DeleterT +> +{ + static_assert(X4Attribute); + static_assert(!X4UnusedAttribute, "*_ptr_parser with `unused_type` is meaningless"); + + using element_type = T; + using deleter_type = DeleterT; + using attribute_type = std::shared_ptr; + + static constexpr bool requires_exact_attribute_type = true; + + using shared_ptr_parser::shared_ptr_parser_base::shared_ptr_parser_base; + + [[nodiscard]] constexpr std::string get_x4_info() const + { + return std::format("shared_ptr({})", get_info{}(this->subject)); + } +}; + +namespace parsers { + +template +[[nodiscard]] constexpr auto shared_ptr(Subject&& subject) + noexcept( + is_parser_nothrow_castable_v && + std::is_nothrow_constructible_v< + shared_ptr_parser>, + as_parser_t + > + ) +{ + return shared_ptr_parser>{ + as_parser(std::forward(subject)) + }; +} + +template, X4Subject Subject> +[[nodiscard]] constexpr auto shared_ptr(Subject&& subject) + noexcept( + is_parser_nothrow_castable_v && + std::is_nothrow_constructible_v< + shared_ptr_parser, T, DeleterT>, + as_parser_t + > + ) +{ + return shared_ptr_parser, T, DeleterT>{ + as_parser(std::forward(subject)) + }; +} + +// ^^^ These cannot be CPO because we want to overload them via template parameter + +} // parsers + +using parsers::shared_ptr; + +} // iris::x4 + +#endif diff --git a/include/iris/x4/core/parser.hpp b/include/iris/x4/core/parser.hpp index 988e0f555..24ad68ba5 100644 --- a/include/iris/x4/core/parser.hpp +++ b/include/iris/x4/core/parser.hpp @@ -214,28 +214,28 @@ struct as_parser_fn template [[nodiscard]] static constexpr auto&& - operator()(parser& p) noexcept + operator()(parser& p IRIS_LIFETIMEBOUND) noexcept { return p.derived(); } template [[nodiscard]] static constexpr auto&& - operator()(parser const& p) noexcept + operator()(parser const& p IRIS_LIFETIMEBOUND) noexcept { return p.derived(); } template [[nodiscard]] static constexpr auto&& - operator()(parser&& p) noexcept + operator()(parser&& p IRIS_LIFETIMEBOUND) noexcept { return std::move(p).derived(); } template [[nodiscard]] static constexpr auto&& - operator()(parser const&& p) noexcept + operator()(parser const&& p IRIS_LIFETIMEBOUND) noexcept { return std::move(p).derived(); } diff --git a/include/iris/x4/debug/print_attribute.hpp b/include/iris/x4/debug/print_attribute.hpp index ee71588ce..67580e866 100644 --- a/include/iris/x4/debug/print_attribute.hpp +++ b/include/iris/x4/debug/print_attribute.hpp @@ -23,9 +23,12 @@ #include +#include + #include #include -#include +#include +#include #include @@ -220,8 +223,23 @@ struct print_attribute_debug static void call(std::ostream& out, traits::CategorizedAttr auto const& val) { - if constexpr (std::formattable) { + if constexpr ( + std::disjunction_v< + std::is_pointer, + is_ttp_specialization_of, + is_ttp_specialization_of + > + ) { + auto const* ptr = std::to_address(val); + if (!ptr) { + out << "nullptr"; + } else { + print_attribute_debug::element_type>::call(out, *ptr); + } + + } else if constexpr (std::formattable) { std::format_to(std::ostreambuf_iterator{out}, "{}", val); + } else { // TODO: https://github.com/iris-cpp/iris/issues/51 //static_assert(iris::req::ADL_ostreamable_v); diff --git a/test/x4/CMakeLists.txt b/test/x4/CMakeLists.txt index 0ea63393f..bf0d0e4e5 100644 --- a/test/x4/CMakeLists.txt +++ b/test/x4/CMakeLists.txt @@ -87,6 +87,7 @@ x4_define_tests( seek sequence skip + smart_ptr substitution symbols1 symbols2 diff --git a/test/x4/smart_ptr.cpp b/test/x4/smart_ptr.cpp new file mode 100644 index 000000000..dff2ecbcd --- /dev/null +++ b/test/x4/smart_ptr.cpp @@ -0,0 +1,227 @@ +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + Distributed under the Boost Software License, Version 1.0. (See accompanying + file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +=============================================================================*/ + +#include "iris_x4_test.hpp" + +#include +#include +#include + +#include + +template +struct custom_deleter +{ + static constexpr void operator()(T* ptr) noexcept + { + delete ptr; + } +}; + +TEST_CASE("unique_ptr (std::default_delete)") +{ + using x4::unique_ptr; + using x4::int_; + using x4::eps; + + // Initial ptr = nullptr + { + std::unique_ptr result; + REQUIRE(parse("1", unique_ptr(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::unique_ptr result; + REQUIRE_FALSE(parse("1", unique_ptr(eps(false)), result)); + REQUIRE(!result); + } + { + std::unique_ptr result; + REQUIRE_THROWS_AS( + parse("1", unique_ptr(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!result); + } + + // Initial ptr = some valid value + { + std::unique_ptr result = std::make_unique(42); + REQUIRE(parse("1", unique_ptr(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::unique_ptr result = std::make_unique(42); + REQUIRE_FALSE(parse("1", unique_ptr(eps(false)), result)); + REQUIRE(!!result); + CHECK(*result == 42); + } + { + std::unique_ptr result = std::make_unique(42); + REQUIRE_THROWS_AS( + parse("1", unique_ptr(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!!result); + CHECK(*result == 42); + } +} + +TEST_CASE("unique_ptr (custom deleter)") +{ + using x4::unique_ptr; + using x4::int_; + using x4::eps; + + // Initial ptr = nullptr + { + std::unique_ptr> result; + REQUIRE(parse("1", unique_ptr>(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::unique_ptr> result; + REQUIRE_FALSE(parse("1", unique_ptr>(eps(false)), result)); + REQUIRE(!result); + } + { + std::unique_ptr> result; + REQUIRE_THROWS_AS( + parse("1", unique_ptr>(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!result); + } + + // Initial ptr = some valid value + { + std::unique_ptr> result(new int(42), custom_deleter{}); + REQUIRE(parse("1", unique_ptr>(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::unique_ptr> result(new int(42), custom_deleter{}); + REQUIRE_FALSE(parse("1", unique_ptr>(eps(false)), result)); + REQUIRE(!!result); + CHECK(*result == 42); + } + { + std::unique_ptr> result(new int(42), custom_deleter{}); + REQUIRE_THROWS_AS( + parse("1", unique_ptr>(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!!result); + CHECK(*result == 42); + } +} + +TEST_CASE("shared_ptr (std::default_delete)") +{ + using x4::shared_ptr; + using x4::int_; + using x4::eps; + + // Initial ptr = nullptr + { + std::shared_ptr result; + REQUIRE(parse("1", shared_ptr(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::shared_ptr result; + REQUIRE_FALSE(parse("1", shared_ptr(eps(false)), result)); + REQUIRE(!result); + } + { + std::shared_ptr result; + REQUIRE_THROWS_AS( + parse("1", shared_ptr(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!result); + } + + // Initial ptr = some valid value + { + std::shared_ptr result = std::make_shared(42); + REQUIRE(parse("1", shared_ptr(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::shared_ptr result = std::make_shared(42); + REQUIRE_FALSE(parse("1", shared_ptr(eps(false)), result)); + REQUIRE(!!result); + CHECK(*result == 42); + } + { + std::shared_ptr result = std::make_shared(42); + REQUIRE_THROWS_AS( + parse("1", shared_ptr(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!!result); + CHECK(*result == 42); + } +} + +TEST_CASE("shared_ptr (custom deleter)") +{ + using x4::shared_ptr; + using x4::int_; + using x4::eps; + + // Initial ptr = nullptr + { + std::shared_ptr result; + REQUIRE(parse("1", shared_ptr>(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::shared_ptr result; + REQUIRE_FALSE(parse("1", shared_ptr>(eps(false)), result)); + REQUIRE(!result); + } + { + std::shared_ptr result; + REQUIRE_THROWS_AS( + parse("1", shared_ptr>(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!result); + } + + // Initial ptr = some valid value + { + std::shared_ptr result(new int(42), custom_deleter{}); + REQUIRE(parse("1", shared_ptr>(int_), result)); + REQUIRE(!!result); + CHECK(*result == 1); + } + { + std::shared_ptr result(new int(42), custom_deleter{}); + REQUIRE_FALSE(parse("1", shared_ptr>(eps(false)), result)); + REQUIRE(!!result); + CHECK(*result == 42); + } + { + std::shared_ptr result(new int(42), custom_deleter{}); + REQUIRE_THROWS_AS( + parse("1", shared_ptr>(eps[([] { throw std::runtime_error{"failed"}; })]), result), + std::runtime_error + ); + REQUIRE(!!result); + CHECK(*result == 42); + } +}