From d451c25b2a9ffb53241f80a7c81964a7b488880c Mon Sep 17 00:00:00 2001 From: Ben Deane Date: Tue, 1 Jul 2025 10:07:10 -0600 Subject: [PATCH] :art: Let `intrusive_list` be instantiated with incomplete types Problem: - `intrusive_list` and `forward_intrusive_list` cannot be instantiated with incomplete types in C++20, because the constraints do not allow it. Since these lists use pointers, it's often useful to declare them using incomplete types. Assuming that the types are complete before actually using the list, this should be fine. Solution: - Add `complete` concept and `is_complete_v` variable template. - Allow use of incomplete types to instantiate `intrusive_list` and `intrusive_forward_list`. --- docs/concepts.adoc | 12 ++++++++++++ docs/intrusive_forward_list.adoc | 4 ++++ docs/intrusive_list.adoc | 6 ++++++ docs/type_traits.adoc | 13 +++++++++++++ include/stdx/concepts.hpp | 8 ++++++++ include/stdx/detail/list_common.hpp | 5 +++-- include/stdx/type_traits.hpp | 5 +++++ test/concepts.cpp | 6 ++++++ test/intrusive_forward_list.cpp | 29 ++++++++++++++++++++++++----- test/intrusive_list.cpp | 16 ++++++++++++++++ test/type_traits.cpp | 6 ++++++ 11 files changed, 103 insertions(+), 7 deletions(-) diff --git a/docs/concepts.adoc b/docs/concepts.adoc index bc5ce78..dd12917 100644 --- a/docs/concepts.adoc +++ b/docs/concepts.adoc @@ -46,6 +46,18 @@ auto generic_lambda = [] (auto i) { return i + 1; }; static_assert(stdx::callable); ---- +=== `complete` + +`complete` is a concept modelled by complete types. + +[source,cpp] +---- +struct incomplete; // not yet defined, not complete + +static_assert(not stdx::complete); +static_assert(stdx::complete); +---- + === `has_trait` `has_trait` is used to turn a type trait (standard or otherwise) into a concept. diff --git a/docs/intrusive_forward_list.adoc b/docs/intrusive_forward_list.adoc index 72c8e56..796b1c9 100644 --- a/docs/intrusive_forward_list.adoc +++ b/docs/intrusive_forward_list.adoc @@ -31,3 +31,7 @@ bool b = l.empty(); `intrusive_forward_list` supports the same xref:intrusive_list.adoc#_node_validity_checking[node validation policy] arguments as `intrusive_list`. + +Like `intrusive_list`, `intrusive_forward_list` requires its node type to have a +`next` pointer of the appropriate type. But it can also be instantiated with an +incomplete type. diff --git a/docs/intrusive_list.adoc b/docs/intrusive_list.adoc index 7e7b3ee..8fd7c80 100644 --- a/docs/intrusive_list.adoc +++ b/docs/intrusive_list.adoc @@ -33,6 +33,12 @@ l.clear(); bool b = l.empty(); ---- +NOTE: An `intrusive_list` requires its node type to have `prev` and `next` +pointers of the appropriate type, and this is enforced by concept constraints +after C++20. However, an `intrusive_list` can also be instantiated with an +incomplete type. Of course the type must be complete at the point of using the +list. + === Node validity checking An `intrusive_list` has a second template parameter which is whether to operate diff --git a/docs/type_traits.adoc b/docs/type_traits.adoc index 8f6d7cb..b275c6b 100644 --- a/docs/type_traits.adoc +++ b/docs/type_traits.adoc @@ -68,6 +68,19 @@ auto y = stdx::apply_sequence([&] () { y += V; }); NOTE: If the function iterates the pack by folding over `operator,` then xref:type_traits.adoc#_template_for_each[`template_for_each`] is probably what you want. +=== `is_complete_v` + +`is_complete_v` is a variable template that is true for complete types and false +for incomplete types. + +[source,cpp] +---- +struct incomplete; // not yet defined, not complete + +static_assert(not stdx::is_complete_v); +static_assert(stdx::is_complete_v); +---- + === `is_function_object_v` `is_function_object_v` is a variable template that detects whether a type is a diff --git a/include/stdx/concepts.hpp b/include/stdx/concepts.hpp index b7287c3..cc0b002 100644 --- a/include/stdx/concepts.hpp +++ b/include/stdx/concepts.hpp @@ -108,6 +108,8 @@ constexpr auto has_trait = TypeTrait::value; template constexpr auto structural = is_structural_v; +template constexpr auto complete = is_complete_v; + #else // After C++20, we can define concepts that are lacking in the library @@ -194,6 +196,9 @@ concept has_trait = TypeTrait::value; template concept structural = is_structural_v; +template +concept complete = is_complete_v; + #endif } // namespace v1 @@ -234,6 +239,9 @@ concept same_as_unqualified = template concept structural = is_structural_v; +template +concept complete = is_complete_v; + template constexpr auto same_any = (... or same_as); diff --git a/include/stdx/detail/list_common.hpp b/include/stdx/detail/list_common.hpp index 224bab3..d197ce7 100644 --- a/include/stdx/detail/list_common.hpp +++ b/include/stdx/detail/list_common.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -23,13 +24,13 @@ concept base_double_linkable = base_single_linkable and requires(T node) { } // namespace detail template -concept single_linkable = requires(T *node) { +concept single_linkable = not complete or requires(T *node) { requires detail::base_single_linkable< std::remove_cvref_tnext)>>; }; template -concept double_linkable = requires(T *node) { +concept double_linkable = not complete or requires(T *node) { requires detail::base_double_linkable< std::remove_cvref_tnext)>>; requires detail::base_double_linkable< diff --git a/include/stdx/type_traits.hpp b/include/stdx/type_traits.hpp index 488955f..56150d5 100644 --- a/include/stdx/type_traits.hpp +++ b/include/stdx/type_traits.hpp @@ -270,5 +270,10 @@ constexpr auto nth_v = #endif STDX_PRAGMA(diagnostic pop) #endif + +template constexpr auto is_complete_v = false; +template +constexpr auto is_complete_v> = true; + } // namespace v1 } // namespace stdx diff --git a/test/concepts.cpp b/test/concepts.cpp index e7f4655..f2cdb4b 100644 --- a/test/concepts.cpp +++ b/test/concepts.cpp @@ -176,3 +176,9 @@ TEST_CASE("structural", "[type_traits]") { STATIC_REQUIRE(stdx::structural); STATIC_REQUIRE(not stdx::structural); } + +TEST_CASE("complete", "[type_traits]") { + struct incomplete; + STATIC_REQUIRE(stdx::complete); + STATIC_REQUIRE(not stdx::complete); +} diff --git a/test/intrusive_forward_list.cpp b/test/intrusive_forward_list.cpp index 9cf8e9b..2951a02 100644 --- a/test/intrusive_forward_list.cpp +++ b/test/intrusive_forward_list.cpp @@ -111,7 +111,7 @@ TEST_CASE("begin", "[intrusive_forward_list]") { CHECK(std::cbegin(list)->value == 1); } -TEST_CASE("front and back", "[intrusive_list]") { +TEST_CASE("front and back", "[intrusive_forward_list]") { stdx::intrusive_forward_list list{}; int_node n1{1}; int_node n2{2}; @@ -226,7 +226,8 @@ TEST_CASE("checked operation clears pointers on pop", CHECK(n1.next == nullptr); } -TEST_CASE("checked operation clears pointers on clear", "[intrusive_list]") { +TEST_CASE("checked operation clears pointers on clear", + "[intrusive_forward_list]") { stdx::intrusive_forward_list list{}; int_node n1{1}; int_node n2{2}; @@ -265,7 +266,8 @@ struct injected_handler { template <> inline auto stdx::panic_handler<> = injected_handler{}; #if __cplusplus >= 202002L -TEST_CASE("checked panic when pushing populated node", "[intrusive_list]") { +TEST_CASE("checked panic when pushing populated node", + "[intrusive_forward_list]") { stdx::intrusive_forward_list list{}; int_node n{5}; @@ -281,7 +283,8 @@ TEST_CASE("checked panic when pushing populated node", "[intrusive_list]") { CHECK(compile_time_calls == 1); } #else -TEST_CASE("checked panic when pushing populated node", "[intrusive_list]") { +TEST_CASE("checked panic when pushing populated node", + "[intrusive_forward_list]") { stdx::intrusive_forward_list list{}; int_node n{5}; @@ -298,7 +301,8 @@ TEST_CASE("checked panic when pushing populated node", "[intrusive_list]") { } #endif -TEST_CASE("unchecked operation doesn't clear pointers", "[intrusive_list]") { +TEST_CASE("unchecked operation doesn't clear pointers", + "[intrusive_forward_list]") { stdx::intrusive_forward_list list{}; int_node n1{1}; int_node n2{2}; @@ -309,3 +313,18 @@ TEST_CASE("unchecked operation doesn't clear pointers", "[intrusive_list]") { CHECK(list.pop_front() == &n1); CHECK(n1.next == before); } + +TEST_CASE("intrusive_forward_list can be instantiated with incomplete types", + "[intrusive_forward_list]") { + struct incomplete_int_node; + stdx::intrusive_forward_list list{}; + + struct incomplete_int_node { + int value{}; + incomplete_int_node *next{}; + }; + + incomplete_int_node n1{1}; + list.push_back(&n1); + CHECK(list.pop_front() == &n1); +} diff --git a/test/intrusive_list.cpp b/test/intrusive_list.cpp index ac13b35..c79042b 100644 --- a/test/intrusive_list.cpp +++ b/test/intrusive_list.cpp @@ -517,3 +517,19 @@ TEST_CASE("insert use case", "[intrusive_list]") { ++it; CHECK(it == std::cend(list)); } + +TEST_CASE("intrusive_list can be instantiated with incomplete types", + "[intrusive_list]") { + struct incomplete_int_node; + stdx::intrusive_list list{}; + + struct incomplete_int_node { + int value{}; + incomplete_int_node *prev{}; + incomplete_int_node *next{}; + }; + + incomplete_int_node n1{1}; + list.push_back(&n1); + CHECK(list.pop_front() == &n1); +} diff --git a/test/type_traits.cpp b/test/type_traits.cpp index b32843c..6c70096 100644 --- a/test/type_traits.cpp +++ b/test/type_traits.cpp @@ -269,3 +269,9 @@ TEST_CASE("nth value in pack", "[type_traits]") { STATIC_REQUIRE(stdx::nth_v<2, 0, true, 'b', 3> == 'b'); } #endif + +TEST_CASE("is_complete_v", "[type_traits]") { + struct incomplete; + STATIC_REQUIRE(stdx::is_complete_v); + STATIC_REQUIRE(not stdx::is_complete_v); +}