Skip to content

Commit f701654

Browse files
Introduce recursive_container_traits (#4623)
* Testing * Similar fix for std::vector * Fix infinite recursion check: 1) Apply to is_copy_assignable additionally 2) Check infinite recursion for map-like types * style: pre-commit fixes * Optional commit that demonstrates the limitations of this PR * Fix positioning of container bindings The bindings were previously in a block that was only activated if numpy was available. * Suggestions from code review: API side * Suggestions from code review: Test side * Suggestions from code review 1) Renaming: is_recursive_container and MutuallyRecursiveContainerPair(MV|VM) 2) Avoid ambiguous specializations of is_recursive_container * Some little fixes * Reordering of structs * Add recursive checks for is_move_constructible * Static testing for pybind11 type traits * 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. * Review suggestions 1. Use std::conditional 2. Fix typo * Remove leftover include from test --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b3e88ec commit f701654

File tree

5 files changed

+484
-27
lines changed

5 files changed

+484
-27
lines changed

include/pybind11/detail/type_caster_base.h

Lines changed: 179 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -822,26 +822,179 @@ 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-
// std::is_copy_constructible isn't quite enough: it lets std::vector<T> (and similar) through when
826-
// T is non-copyable, but code containing such a copy constructor fails to actually compile.
827-
template <typename T, typename SFINAE = void>
828-
struct is_copy_constructible : std::is_copy_constructible<T> {};
825+
// Does the container have a mapped type and is it recursive?
826+
// Implemented by specializations below.
827+
template <typename Container, typename SFINAE = void>
828+
struct container_mapped_type_traits {
829+
static constexpr bool has_mapped_type = false;
830+
static constexpr bool has_recursive_mapped_type = false;
831+
};
832+
833+
template <typename Container>
834+
struct container_mapped_type_traits<
835+
Container,
836+
typename std::enable_if<
837+
std::is_same<typename Container::mapped_type, Container>::value>::type> {
838+
static constexpr bool has_mapped_type = true;
839+
static constexpr bool has_recursive_mapped_type = true;
840+
};
841+
842+
template <typename Container>
843+
struct container_mapped_type_traits<
844+
Container,
845+
typename std::enable_if<
846+
negation<std::is_same<typename Container::mapped_type, Container>>::value>::type> {
847+
static constexpr bool has_mapped_type = true;
848+
static constexpr bool has_recursive_mapped_type = false;
849+
};
850+
851+
// Does the container have a value type and is it recursive?
852+
// Implemented by specializations below.
853+
template <typename Container, typename SFINAE = void>
854+
struct container_value_type_traits : std::false_type {
855+
static constexpr bool has_value_type = false;
856+
static constexpr bool has_recursive_value_type = false;
857+
};
858+
859+
template <typename Container>
860+
struct container_value_type_traits<
861+
Container,
862+
typename std::enable_if<
863+
std::is_same<typename Container::value_type, Container>::value>::type> {
864+
static constexpr bool has_value_type = true;
865+
static constexpr bool has_recursive_value_type = true;
866+
};
867+
868+
template <typename Container>
869+
struct container_value_type_traits<
870+
Container,
871+
typename std::enable_if<
872+
negation<std::is_same<typename Container::value_type, Container>>::value>::type> {
873+
static constexpr bool has_value_type = true;
874+
static constexpr bool has_recursive_value_type = false;
875+
};
876+
877+
/*
878+
* Tag to be used for representing the bottom of recursively defined types.
879+
* Define this tag so we don't have to use void.
880+
*/
881+
struct recursive_bottom {};
882+
883+
/*
884+
* Implementation detail of `recursive_container_traits` below.
885+
* `T` is the `value_type` of the container, which might need to be modified to
886+
* avoid recursive types and const types.
887+
*/
888+
template <typename T, bool is_this_a_map>
889+
struct impl_type_to_check_recursively {
890+
/*
891+
* If the container is recursive, then no further recursion should be done.
892+
*/
893+
using if_recursive = recursive_bottom;
894+
/*
895+
* Otherwise yield `T` unchanged.
896+
*/
897+
using if_not_recursive = T;
898+
};
899+
900+
/*
901+
* For pairs - only as value type of a map -, the first type should remove the `const`.
902+
* Also, if the map is recursive, then the recursive checking should consider
903+
* the first type only.
904+
*/
905+
template <typename A, typename B>
906+
struct impl_type_to_check_recursively<std::pair<A, B>, /* is_this_a_map = */ true> {
907+
using if_recursive = typename std::remove_const<A>::type;
908+
using if_not_recursive = std::pair<typename std::remove_const<A>::type, B>;
909+
};
829910

830-
template <typename T, typename SFINAE = void>
831-
struct is_move_constructible : std::is_move_constructible<T> {};
911+
/*
912+
* Implementation of `recursive_container_traits` below.
913+
*/
914+
template <typename Container, typename SFINAE = void>
915+
struct impl_recursive_container_traits {
916+
using type_to_check_recursively = recursive_bottom;
917+
};
832918

833-
// Specialization for types that appear to be copy constructible but also look like stl containers
834-
// (we specifically check for: has `value_type` and `reference` with `reference = value_type&`): if
835-
// so, copy constructability depends on whether the value_type is copy constructible.
836919
template <typename Container>
837-
struct is_copy_constructible<
920+
struct impl_recursive_container_traits<
838921
Container,
839-
enable_if_t<
840-
all_of<std::is_copy_constructible<Container>,
841-
std::is_same<typename Container::value_type &, typename Container::reference>,
842-
// Avoid infinite recursion
843-
negation<std::is_same<Container, typename Container::value_type>>>::value>>
844-
: is_copy_constructible<typename Container::value_type> {};
922+
typename std::enable_if<container_value_type_traits<Container>::has_value_type>::type> {
923+
static constexpr bool is_recursive
924+
= container_mapped_type_traits<Container>::has_recursive_mapped_type
925+
|| container_value_type_traits<Container>::has_recursive_value_type;
926+
/*
927+
* This member dictates which type Pybind11 should check recursively in traits
928+
* such as `is_move_constructible`, `is_copy_constructible`, `is_move_assignable`, ...
929+
* Direct access to `value_type` should be avoided:
930+
* 1. `value_type` might recursively contain the type again
931+
* 2. `value_type` of STL map types is `std::pair<A const, B>`, the `const`
932+
* should be removed.
933+
*
934+
*/
935+
using type_to_check_recursively = typename std::conditional<
936+
is_recursive,
937+
typename impl_type_to_check_recursively<
938+
typename Container::value_type,
939+
container_mapped_type_traits<Container>::has_mapped_type>::if_recursive,
940+
typename impl_type_to_check_recursively<
941+
typename Container::value_type,
942+
container_mapped_type_traits<Container>::has_mapped_type>::if_not_recursive>::type;
943+
};
944+
945+
/*
946+
* This trait defines the `type_to_check_recursively` which is needed to properly
947+
* handle recursively defined traits such as `is_move_constructible` without going
948+
* into an infinite recursion.
949+
* Should be used instead of directly accessing the `value_type`.
950+
* It cancels the recursion by returning the `recursive_bottom` tag.
951+
*
952+
* The default definition of `type_to_check_recursively` is as follows:
953+
*
954+
* 1. By default, it is `recursive_bottom`, so that the recursion is canceled.
955+
* 2. If the type is non-recursive and defines a `value_type`, then the `value_type` is used.
956+
* If the `value_type` is a pair and a `mapped_type` is defined,
957+
* then the `const` is removed from the first type.
958+
* 3. If the type is recursive and `value_type` is not a pair, then `recursive_bottom` is returned.
959+
* 4. If the type is recursive and `value_type` is a pair and a `mapped_type` is defined,
960+
* then `const` is removed from the first type and the first type is returned.
961+
*
962+
* This behavior can be extended by the user as seen in test_stl_binders.cpp.
963+
*
964+
* This struct is exactly the same as impl_recursive_container_traits.
965+
* The duplication achieves that user-defined specializations don't compete
966+
* with internal specializations, but take precedence.
967+
*/
968+
template <typename Container, typename SFINAE = void>
969+
struct recursive_container_traits : impl_recursive_container_traits<Container> {};
970+
971+
template <typename T>
972+
struct is_move_constructible
973+
: all_of<std::is_move_constructible<T>,
974+
is_move_constructible<
975+
typename recursive_container_traits<T>::type_to_check_recursively>> {};
976+
977+
template <>
978+
struct is_move_constructible<recursive_bottom> : std::true_type {};
979+
980+
// Likewise for std::pair
981+
// (after C++17 it is mandatory that the move constructor not exist when the two types aren't
982+
// themselves move constructible, but this can not be relied upon when T1 or T2 are themselves
983+
// containers).
984+
template <typename T1, typename T2>
985+
struct is_move_constructible<std::pair<T1, T2>>
986+
: all_of<is_move_constructible<T1>, is_move_constructible<T2>> {};
987+
988+
// std::is_copy_constructible isn't quite enough: it lets std::vector<T> (and similar) through when
989+
// T is non-copyable, but code containing such a copy constructor fails to actually compile.
990+
template <typename T>
991+
struct is_copy_constructible
992+
: all_of<std::is_copy_constructible<T>,
993+
is_copy_constructible<
994+
typename recursive_container_traits<T>::type_to_check_recursively>> {};
995+
996+
template <>
997+
struct is_copy_constructible<recursive_bottom> : std::true_type {};
845998

846999
// Likewise for std::pair
8471000
// (after C++17 it is mandatory that the copy constructor not exist when the two types aren't
@@ -852,14 +1005,16 @@ struct is_copy_constructible<std::pair<T1, T2>>
8521005
: all_of<is_copy_constructible<T1>, is_copy_constructible<T2>> {};
8531006

8541007
// The same problems arise with std::is_copy_assignable, so we use the same workaround.
855-
template <typename T, typename SFINAE = void>
856-
struct is_copy_assignable : std::is_copy_assignable<T> {};
857-
template <typename Container>
858-
struct is_copy_assignable<Container,
859-
enable_if_t<all_of<std::is_copy_assignable<Container>,
860-
std::is_same<typename Container::value_type &,
861-
typename Container::reference>>::value>>
862-
: is_copy_assignable<typename Container::value_type> {};
1008+
template <typename T>
1009+
struct is_copy_assignable
1010+
: all_of<
1011+
std::is_copy_assignable<T>,
1012+
is_copy_assignable<typename recursive_container_traits<T>::type_to_check_recursively>> {
1013+
};
1014+
1015+
template <>
1016+
struct is_copy_assignable<recursive_bottom> : std::true_type {};
1017+
8631018
template <typename T1, typename T2>
8641019
struct is_copy_assignable<std::pair<T1, T2>>
8651020
: all_of<is_copy_assignable<T1>, is_copy_assignable<T2>> {};

include/pybind11/stl_bind.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +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, enable_if_t<container_traits<T>::is_vector>> {
65-
static constexpr const bool value = is_comparable<typename T::value_type>::value;
66-
};
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> {};
66+
67+
template <>
68+
struct is_comparable<recursive_bottom> : std::true_type {};
6769

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

0 commit comments

Comments
 (0)