Skip to content

Commit 46cfdbb

Browse files
committed
More precise checking of recursive types
Instead of a trait `is_recursive_container`, use a trait `recursive_container_traits` with dependent type `recursive_container_traits::type_to_check_recursively`. So, instead of just checking if a type is recursive and then trying to somehow deal with it, recursively-defined traits such as is_move_constructible can now directly ask this trait where the recursion should proceed.
1 parent eba6239 commit 46cfdbb

File tree

3 files changed

+161
-78
lines changed

3 files changed

+161
-78
lines changed

include/pybind11/detail/type_caster_base.h

Lines changed: 148 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -822,56 +822,157 @@ using movable_cast_op_type
822822
typename std::add_rvalue_reference<intrinsic_t<T>>::type,
823823
typename std::add_lvalue_reference<intrinsic_t<T>>::type>>;
824824

825+
template <bool Condition, typename Then, typename Else>
826+
struct if_then_else {};
827+
828+
template <typename Then, typename Else>
829+
struct if_then_else<true, Then, Else> {
830+
using type = Then;
831+
};
832+
833+
template <typename Then, typename Else>
834+
struct if_then_else<false, Then, Else> {
835+
using type = Else;
836+
};
837+
825838
// True if Container has a dependent type mapped_type that is equivalent
826839
// to Container itself
827840
// Actual implementation in the SFINAE specialization below
828-
template <typename Container, typename MappedType = Container>
829-
struct is_container_with_recursive_mapped_type : std::false_type {};
841+
template <typename Container, typename SFINAE = void>
842+
struct container_mapped_type_traits {
843+
static constexpr bool has_mapped_type = false;
844+
static constexpr bool has_recursive_mapped_type = false;
845+
};
830846

831847
// This specialization is only valid if both conditions are fulfilled:
832848
// 1) The mapped_type exists
833849
// 2) And it is equivalent to Container
834850
template <typename Container>
835-
struct is_container_with_recursive_mapped_type<Container, typename Container::mapped_type>
836-
: std::true_type {};
851+
struct container_mapped_type_traits<
852+
Container,
853+
typename std::enable_if<
854+
std::is_same<typename Container::mapped_type, Container>::value>::type> {
855+
static constexpr bool has_mapped_type = true;
856+
static constexpr bool has_recursive_mapped_type = true;
857+
};
858+
859+
template <typename Container>
860+
struct container_mapped_type_traits<
861+
Container,
862+
typename std::enable_if<
863+
negation<std::is_same<typename Container::mapped_type, Container>>::value>::type> {
864+
static constexpr bool has_mapped_type = true;
865+
static constexpr bool has_recursive_mapped_type = false;
866+
};
837867

838868
// True if Container has a dependent type value_type that is equivalent
839869
// to Container itself
840870
// Actual implementation in the SFINAE specialization below
841-
template <typename Container, typename MappedType = Container>
842-
struct is_container_with_recursive_value_type : std::false_type {};
871+
template <typename Container, typename SFINAE = void>
872+
struct container_value_type_traits : std::false_type {
873+
static constexpr bool has_value_type = false;
874+
static constexpr bool has_recursive_value_type = false;
875+
};
843876

844877
// This specialization is only valid if both conditions are fulfilled:
845878
// 1) The value_type exists
846879
// 2) And it is equivalent to Container
847880
template <typename Container>
848-
struct is_container_with_recursive_value_type<Container, typename Container::value_type>
849-
: std::true_type {};
850-
851-
// True constant if the type contains itself recursively.
852-
// By default, this will check the mapped_type and value_type dependent types.
853-
// In more complex recursion patterns, users can specialize this struct.
854-
// The second template parameter SFINAE=void is for use of std::enable_if in specializations.
855-
// An example is found in tests/test_stl_binders.cpp.
856-
template <typename Container, typename SFINAE = void>
857-
struct is_recursive_container : any_of<is_container_with_recursive_value_type<Container>,
858-
is_container_with_recursive_mapped_type<Container>> {};
881+
struct container_value_type_traits<
882+
Container,
883+
typename std::enable_if<
884+
std::is_same<typename Container::value_type, Container>::value>::type> {
885+
static constexpr bool has_value_type = true;
886+
static constexpr bool has_recursive_value_type = true;
887+
};
888+
889+
template <typename Container>
890+
struct container_value_type_traits<
891+
Container,
892+
typename std::enable_if<
893+
negation<std::is_same<typename Container::value_type, Container>>::value>::type> {
894+
static constexpr bool has_value_type = true;
895+
static constexpr bool has_recursive_value_type = false;
896+
};
859897

860-
template <typename T, typename SFINAE = void>
861-
struct is_move_constructible : std::is_move_constructible<T> {};
898+
/*
899+
* Tag to be used for representing the bottom of recursively defined types.
900+
* Define this tag so we don't have to use void.
901+
*/
902+
struct recursive_bottom {};
903+
904+
/*
905+
* Implementation detail of `recursive_container_traits` below.
906+
* `T` is the `value_type` of the container, which might need to be modified to
907+
* avoid recursive types and const types.
908+
*/
909+
template <typename T>
910+
struct impl_type_to_check_recursively {
911+
/*
912+
* If the container is recursive, then no further recursion should be done.
913+
*/
914+
using if_recursive = recursive_bottom;
915+
/*
916+
* Otherwise yield `T` unchanged.
917+
*/
918+
using if_not_recursive = T;
919+
};
920+
921+
/*
922+
* For pairs, the first type should remove the `const`.
923+
* Also, if the container is recursive, then the recursive checking should consider
924+
* the first type only.
925+
*/
926+
template <typename A, typename B>
927+
struct impl_type_to_check_recursively<std::pair<A, B>> {
928+
using if_recursive = typename std::remove_const<A>::type;
929+
using if_not_recursive = std::pair<typename std::remove_const<A>::type, B>;
930+
};
931+
932+
template <typename Container, typename SFINAE = void>
933+
struct impl_recursive_container_traits {
934+
using type_to_check_recursively = recursive_bottom;
935+
};
862936

863-
// Specialization for types that appear to be move constructible but also look like stl containers
864-
// (we specifically check for: has `value_type` and `reference` with `reference = value_type&`): if
865-
// so, move constructability depends on whether the value_type is move constructible.
866937
template <typename Container>
867-
struct is_move_constructible<
938+
struct impl_recursive_container_traits<
868939
Container,
869-
enable_if_t<
870-
all_of<std::is_move_constructible<Container>,
871-
std::is_same<typename Container::value_type &, typename Container::reference>,
872-
// Avoid infinite recursion
873-
negation<is_recursive_container<Container>>>::value>>
874-
: is_move_constructible<typename Container::value_type> {};
940+
typename std::enable_if<container_value_type_traits<Container>::has_value_type>::type> {
941+
static constexpr bool is_recursive
942+
= container_mapped_type_traits<Container>::has_recursive_mapped_type
943+
|| container_value_type_traits<Container>::has_recursive_value_type;
944+
/*
945+
* This member dictates which type Pybind11 should check recursively in traits
946+
* such as `is_move_constructible`, `is_copy_constructible`, `is_move_assignable`, ...
947+
* Direct access to `value_type` should be avoided:
948+
* 1. `value_type` might recursively contain the type again
949+
* 2. `value_type` of STL map types is `std::pair<A const, B>`, the `const`
950+
* should be removed.
951+
*
952+
*/
953+
using type_to_check_recursively = typename if_then_else<
954+
is_recursive,
955+
typename impl_type_to_check_recursively<typename Container::value_type>::if_recursive,
956+
typename impl_type_to_check_recursively<
957+
typename Container::value_type>::if_not_recursive>::type;
958+
};
959+
960+
/*
961+
* This is exactly the same as impl_recursive_container_traits.
962+
* This duplication achieves that user-defined specializations don't compete
963+
* with internal specializations, but take precedence.
964+
*/
965+
template <typename Container, typename SFINAE = void>
966+
struct recursive_container_traits : impl_recursive_container_traits<Container> {};
967+
968+
template <typename T>
969+
struct is_move_constructible
970+
: all_of<std::is_move_constructible<T>,
971+
is_move_constructible<
972+
typename recursive_container_traits<T>::type_to_check_recursively>> {};
973+
974+
template <>
975+
struct is_move_constructible<recursive_bottom> : std::true_type {};
875976

876977
// Likewise for std::pair
877978
// (after C++17 it is mandatory that the move constructor not exist when the two types aren't
@@ -883,21 +984,14 @@ struct is_move_constructible<std::pair<T1, T2>>
883984

884985
// std::is_copy_constructible isn't quite enough: it lets std::vector<T> (and similar) through when
885986
// T is non-copyable, but code containing such a copy constructor fails to actually compile.
886-
template <typename T, typename SFINAE = void>
887-
struct is_copy_constructible : std::is_copy_constructible<T> {};
987+
template <typename T>
988+
struct is_copy_constructible
989+
: all_of<std::is_copy_constructible<T>,
990+
is_copy_constructible<
991+
typename recursive_container_traits<T>::type_to_check_recursively>> {};
888992

889-
// Specialization for types that appear to be copy constructible but also look like stl containers
890-
// (we specifically check for: has `value_type` and `reference` with `reference = value_type&`): if
891-
// so, copy constructability depends on whether the value_type is copy constructible.
892-
template <typename Container>
893-
struct is_copy_constructible<
894-
Container,
895-
enable_if_t<
896-
all_of<std::is_copy_constructible<Container>,
897-
std::is_same<typename Container::value_type &, typename Container::reference>,
898-
// Avoid infinite recursion
899-
negation<is_recursive_container<Container>>>::value>>
900-
: is_copy_constructible<typename Container::value_type> {};
993+
template <>
994+
struct is_copy_constructible<recursive_bottom> : std::true_type {};
901995

902996
// Likewise for std::pair
903997
// (after C++17 it is mandatory that the copy constructor not exist when the two types aren't
@@ -908,24 +1002,19 @@ struct is_copy_constructible<std::pair<T1, T2>>
9081002
: all_of<is_copy_constructible<T1>, is_copy_constructible<T2>> {};
9091003

9101004
// The same problems arise with std::is_copy_assignable, so we use the same workaround.
911-
template <typename T, typename SFINAE = void>
912-
struct is_copy_assignable : std::is_copy_assignable<T> {};
913-
template <typename Container>
914-
struct is_copy_assignable<
915-
Container,
916-
enable_if_t<
917-
all_of<std::is_copy_assignable<Container>,
918-
std::is_same<typename Container::value_type &, typename Container::reference>,
919-
// Avoid infinite recursion
920-
negation<is_recursive_container<Container>>>::value>>
921-
: is_copy_assignable<typename Container::value_type> {};
1005+
template <typename T>
1006+
struct is_copy_assignable
1007+
: all_of<
1008+
std::is_copy_assignable<T>,
1009+
is_copy_assignable<typename recursive_container_traits<T>::type_to_check_recursively>> {
1010+
};
1011+
1012+
template <>
1013+
struct is_copy_assignable<recursive_bottom> : std::true_type {};
1014+
9221015
template <typename T1, typename T2>
9231016
struct is_copy_assignable<std::pair<T1, T2>>
924-
/*
925-
* Need to remove the const qualifier from T1 here since the value_type in
926-
* STL map types is std::pair<const Key, T>, and const types are never assignable
927-
*/
928-
: all_of<is_copy_assignable<typename std::remove_const<T1>::type>, is_copy_assignable<T2>> {};
1017+
: all_of<is_copy_assignable<T1>, is_copy_assignable<T2>> {};
9291018

9301019
PYBIND11_NAMESPACE_END(detail)
9311020

include/pybind11/stl_bind.h

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,11 @@ struct is_comparable<
6161
/* For a vector/map data structure, recursively check the value type
6262
(which is std::pair for maps) */
6363
template <typename T>
64-
struct is_comparable<T,
65-
enable_if_t<container_traits<T>::is_vector &&
66-
// Avoid this instantiation if the type is recursive
67-
negation<is_recursive_container<T>>::value>> {
68-
static constexpr const bool value = is_comparable<typename T::value_type>::value;
69-
};
64+
struct is_comparable<T, enable_if_t<container_traits<T>::is_vector>>
65+
: is_comparable<typename recursive_container_traits<T>::type_to_check_recursively> {};
7066

71-
/* Skip the recursion if the type itself is recursive */
72-
template <typename T>
73-
struct is_comparable<T,
74-
enable_if_t<container_traits<T>::is_vector &&
75-
// Special case: The vector type is recursive
76-
is_recursive_container<T>::value>> {
77-
static constexpr const bool value = container_traits<T>::is_comparable;
78-
};
67+
template <>
68+
struct is_comparable<recursive_bottom> : std::true_type {};
7969

8070
/* For pairs, recursively check the two data types */
8171
template <typename T>

tests/test_stl_binders.cpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ struct RecursiveMap : std::map<int, RecursiveMap> {
8686
/*
8787
* Pybind11 does not catch more complicated recursion schemes, such as mutual
8888
* recursion.
89-
* In that case custom is_recursive_container specializations need to be added,
89+
* In that case custom recursive_container_traits specializations need to be added,
9090
* thus manually telling pybind11 about the recursion.
9191
*/
9292
struct MutuallyRecursiveContainerPairMV;
@@ -98,9 +98,13 @@ struct MutuallyRecursiveContainerPairVM : std::vector<MutuallyRecursiveContainer
9898
namespace pybind11 {
9999
namespace detail {
100100
template <typename SFINAE>
101-
struct is_recursive_container<MutuallyRecursiveContainerPairMV, SFINAE> : std::true_type {};
101+
struct recursive_container_traits<MutuallyRecursiveContainerPairMV, SFINAE> {
102+
using type_to_check_recursively = recursive_bottom;
103+
};
102104
template <typename SFINAE>
103-
struct is_recursive_container<MutuallyRecursiveContainerPairVM, SFINAE> : std::true_type {};
105+
struct recursive_container_traits<MutuallyRecursiveContainerPairVM, SFINAE> {
106+
using type_to_check_recursively = recursive_bottom;
107+
};
104108
} // namespace detail
105109
} // namespace pybind11
106110

@@ -166,8 +170,8 @@ TEST_SUBMODULE(stl_binders, m) {
166170
// Bind recursive container types
167171
py::bind_vector<RecursiveVector>(m, "RecursiveVector");
168172
py::bind_map<RecursiveMap>(m, "RecursiveMap");
169-
py::bind_map<MutuallyRecursiveContainerPairMV>(m, "MutuallyRecursiveContainerPairMV");
170-
py::bind_vector<MutuallyRecursiveContainerPairVM>(m, "MutuallyRecursiveContainerPairVM");
173+
// py::bind_map<MutuallyRecursiveContainerPairMV>(m, "MutuallyRecursiveContainerPairMV");
174+
// py::bind_vector<MutuallyRecursiveContainerPairVM>(m, "MutuallyRecursiveContainerPairVM");
171175

172176
// The rest depends on numpy:
173177
try {

0 commit comments

Comments
 (0)