-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Allow arbitrary class_ template option ordering #385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Actually, that isn't quite right-- |
Removing the order requirement in the That said, I'm on the fence for this one for a couple of reasons: With this patch, there would now be two 100% equivalent ways of specifying the base class (either as a variadic template argument, or via The amount of metatemplate programming that is necessary to pull this off makes for a gratuitously implementation. I expect that this also has an adverse influence on compilation time for big projects, where |
The template logic could be significantly simplified with something like this: template<template<class> class Predicate, class Default, class... Ts>
struct extract_if;
template<template<class> class Predicate, class Default>
struct extract_if<Predicate, Default> { using type = Default; };
template<template<class> class Predicate, class Default, class T, class... Ts>
struct extract_if<Predicate, Default, T, Ts...> : std::conditional<
Predicate<T>::value, T, typename extract_if<Predicate, Default, Ts...>::type
> {};
// Extract the first type that satisfies the Predicate or return Default
template<template<class> class Predicate, class Default, class... Ts>
using extract_if_t = typename extract_if<Predicate, Default, Ts...>::type; Example usage: using holder_type = extract_if_t<is_holder, default_holder, options...>;
using type_alias = extract_if_t<is_subtype, void, options...>; This also avoids dragging in |
@dean0x7d the point of the std::tuple bits was to be able to provide a list of unmatched parameters, to pass through to the next function, and to verify that, after getting out everything, there are no extra parameters. Perhaps the first isn't necessary, though (it seems highly unlikely that a holder class will also be a subclass), and the second ought to be doable by counting the number of matches. I'll investigate. |
You can pair the above static_assert(all_of_t<is_valid_class_option, options...>::value,
"Unknown/invalid class_ template parameters provided"); |
I was thinking of just doing a count instead: static_assert(0 == sizeof...(options) - (int) has_alias - (int) extracted_holder::found - (int) extracted_base::found,
"Unknown/invalid class_ template parameters provided"); |
T, | ||
typename extract_first<Predicate, Ts...>::type | ||
>::type; | ||
constexpr static bool found = Predicate<T>::value || extract_first<Predicate, Ts...>::found; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
found
is probably not needed, can use std::is_same<...::type, void>
below.
This looks simpler now, though I'd still like to run some gcc/clang benchmarks to be sure that this does not unduly increase compilation time. The question about multiple bases remains though. |
I think that going with |
Unless I'm missing something, wouldn't it require another |
Maybe that's not a big deal. If you specify multiple trampolines, or multiple holders, you get undefined behaviour (in practice, everything past the first would be ignored). |
I'm thinking about it, will reply soon. |
(The obvious thing here is to be able to specify |
Yeah, I didn't consider duplicates -- it wouldn't catch those, but I'm not sure how important that is. One other thing to consider: wrapping py::class_<C, py::alias<PyC>, py::bases<A, B>> c(m, "C");
// vs
py::class_<C, PyC, A, B> c(m, "C"); It would also make it simpler to add any future option to (Although, I'm not quite sure why there are 2 ways of specifying bases by C++ type name: template param and constructor param.) |
See the last commit: it allows specification of multiple base classes, currently producing an assertion failure if you specify more than one base. Eventual MI support would then just be a matter of uncommenting the |
Well, I saw this as a replacement for the That said, the |
You're probably right. Switching to |
On the other other hand, if
Predicates in the form of template aliases are extremely cheap (dare I say free). It's preferable to use an alias whenever possible: template<class T> using is_my_class = is_same<MyClass, T>; // alias
// vs.
template<class T> struct is_my_class : is_same<MyClass, T> {}; // new type The alias is much lighter weight than the struct. The latter will instantiate a new type and the compiler needs to be aware of it (uses compiler memory and time). The alias is essentially just a replacement: it does not introduce a new type, thus it cannot be specialized and it does not appear as a unique type for overloads or class specializations. |
Oops, I mean compilation speed, not size. |
I assumed so. Both are compile to nothing. The post above applies to compile performance. |
863de9e
to
1bbfbd7
Compare
Yea, this looks significantly simpler now, and I believe the result is worth the hackery, more extendable in case more template parameter stuff will be added in the future. |
Yes, I use it when possible, but as far as I can see, it can't be used as a partial specialization, along the lines of: template <typename T> class A {};
template <> using A<bool> = std::true_type; |
I suppose I could do something like: template <typename T> class A_impl {};
template <typename T> using A = typename std::conditional<std::is_same<T, bool>::value, std::true_type, A_impl<T>>::type; would this still be worthwhile over: template <typename T> struct A {};
template <> struct A<bool> : std::true_type {}; ? |
If you need specialization, go with the struct. Readability is still important, and besides, the |
I've based this now on PR #372 for its I also got rid of the std::tuple approach for the base classes--it seems to be more compiler friendly in its current form, where it simple iterates through all the options until it find a base class. |
e0f9468
to
93ec1c1
Compare
@wjakob I've uploaded some benchmarking results using g++ and clang++ on linux compiled under -O2, using the benchmark code in These are pathologically bad scenarios (100 to 1000 registered, empty classes declared in a single compilation unit, without anything else declared--just the classes), just to get an idea for the absolute maximum worst case. The quick takeaway is that the compilation time increase is around 0-3%, depending on the case. The worst case for compiler memory use is about 300KB per registered class under g++, and considerably less than that under clang++. That amount seems pretty unlikely to be a noticeable increase when you have many fewer classes and many more (i.e. more than 0) methods, functions, etc.. It's also the "compatibility" case, i.e. where you're uselessly specifying the default unique_ptr; if you omit it, compilation becomes faster and requires less memory. |
Thank you for benchmarking -- those numbers seem quite reasonable. Since it looks like you are still simplifying the template implementation and waiting for #372, I'll merge that when it's ready and then do another pass over your submission. |
If it's easy to package up in a self-contained example, it may be worth submitting it on Microsoft connect. They are usually pretty good at fixing things (thought it will likely take until the next point release in that case) |
Yeah, I think a reproducible example would actually be quite compact. I'll put something together and submit it. |
FYI: #372 is merged now. |
Yeah, I'm working on the rebase right now. |
The current pybind11::class_<Type, Holder, Trampoline> fixed template ordering results in a requirement to repeat the Holder with its default value (std::unique_ptr<Type>) argument, which is a little bit annoying: it needs to be specified not because we want to override the default, but rather because we need to specify the third argument. This commit removes this limitation by making the class_ template take the type name plus a parameter pack of options. It then extracts the first valid holder type and the first subclass type for holder_type and trampoline type_alias, respectively. (If unfound, both fall back to their current defaults, `std::unique_ptr<type>` and `type`, respectively). If any unmatched template arguments are provided, a static assertion fails. What this means is that you can specify or omit the arguments in any order: py::class_<A, PyA> c1(m, "A"); py::class_<B, PyB, std::shared_ptr<B>> c2(m, "B"); py::class_<C, std::shared_ptr<C>, PyB> c3(m, "C"); It also allows future class attributes (such as base types in the next commit) to be passed as class template types rather than needing to use a py::base<> wrapper.
00c0fd5
to
1cb7246
Compare
Rebased and squashed. I included the benchmarking scripts I wrote to benchmark this (in |
@dean0x7d Indeed the workaround is no longer needed (and removed from the squashed commits). |
As for multiple inheritance: it should be dead-easy to add to the interface now to allow specification via |
// PYBIND11_DECLARE_HOLDER_TYPE holder types: | ||
std::conditional<std::is_base_of<detail::type_caster_holder<base, holder>, detail::type_caster<holder>>::value, | ||
std::true_type, | ||
std::false_type>::type {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can inherit directly from is_base_of
and skip conditional
:
struct is_holder_type : std::is_base_of<...> {};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed that was pointlessly complex.
This allows a slightly cleaner base type specification of: py::class_<Type, Base>("Type") as an alternative to py::class_<Type>("Type", py::base<Base>()) As with the other template parameters, the order relative to the holder or trampoline types doesn't matter. This also includes a compile-time assertion failure if attempting to specify more than one base class (but is easily extendible to support multiple inheritance, someday, by updating the class_selector::set_bases function to set multiple bases).
1cb7246
to
f85f2ae
Compare
Squashed those suggestions (but keeping |
I think it would be better to separate out the benchmarking script, which would benefit from some more discussion. |
Other than that, this looks great now -- the readability is much better, and the new infrastructure will be useful for other parts of the library as well. |
f85f2ae
to
6b52c83
Compare
Okay, the benchmarking commit is removed from the PR. |
Great, thank you! |
The current
pybind11::class_<Type, Holder, Trampoline>
fixed template ordering results in a requirement to repeat the Holder with its default value (std::unique_ptr<Type>
) argument, which is a little bit annoying: it needs to be specified not because we want to override the default, but rather just because we need to specify the third argument.This commit removes this limitation by making the class_ template take the type name plus a parameter pack of options. It then extracts the first valid holder type and the first subclass type for holder_type and trampoline type_alias, respectively. (If unfound, both fall back to their current defaults,
std::unique_ptr<type>
andtype
, respectively). If any unmatched template arguments are provided, astatic assertion fails.
What this means is that you can specify or omit the arguments in any order:
It also means that other class attributes can be passed this way—for example, the base type (in the next commit), or the
always_copy
class attribute being discussed in #376.