[math] Use std::function in ROOT::Math::ParamFunctor - #23211
Conversation
2867e5e to
3b30a35
Compare
Test Results 23 files 23 suites 3d 18h 37m 35s ⏱️ For more details on these failures, see this check. Results for commit efb4d48. ♻️ This comment has been updated with latest results. |
`ParamFunctor` was still carrying the hand-rolled type erasure that the other `ROOT::Math` functors got rid of in 6c68bbd and a24465f: a `ParamFunctionBase` interface, `ParamFunctorHandler` and `ParamMemFunHandler` implementations of it, three `FuncEvaluator` partial specialisations to tell pointer types apart, a manual `Clone()`, a raw owning `Impl *` with hand-written copy constructor, assignment operator and destructor, and about 40 lines of commented-out code. All of that is what `std::function` does, and the class already had a `std::function` constructor sitting next to it. Store a single `std::function<T(const T *, const double *)>` instead and let the compiler generate the copy operations. The three callable shapes the `FuncEvaluator` specialisations used to dispatch on are kept by normalising them in one `Adapt()` helper: a callable taking const pointers is stored as is, a callable insisting on non-const pointers (the classic `T (T *x, double *p)` signature) gets them cast for it, and a pointer to a callable object is called through without taking ownership of it. That makes the separate `FreeFunc` constructor redundant, since `Adapt()` already normalises a free function pointer, so it goes. Nothing in ROOT converted a free function to a `ParamFunctor` implicitly. The `std::function` constructor stays implicit, on the other hand, because PyROOT needs it: cppyy binds a Python-side callable to the `TF1(const char *, ROOT::Math::ParamFunctor, ...)` overload through that conversion, and `tutorials/math/fit/fitNormSum.py` fails to find a viable overload without it. Calling a `ParamFunctor` is unchanged, and so is constructing one, with one further exception: the constructor from an object and one of its member functions now takes a plain `Obj *` rather than a `const PtrObj &` that only had to be dereferenceable. Every caller passes a raw pointer, and spelling that out rejects at the signature what used to fail inside the handler. The removed `GetImpl()` and `SetFunction()` were only handles on the deleted `ParamFunctionBase` and had no callers. 🤖 Done with the help of AI
3b30a35 to
efb4d48
Compare
hageboeck
left a comment
There was a problem hiding this comment.
Thanks for the changes! It looks like now we can go even further.
| template <typename Func> | ||
| explicit ParamFunctorTempl(const Func &f) : fFunc{Adapt(f)} |
There was a problem hiding this comment.
How about making this a universal reference using
template<typename Func>
explicit ParamFunctorTempl(Func &&f)This would allow for moving an object into the functor. Then, since after the refactoring (thanks!) Adapt is only used in a single place, we could move its entire body right here.
And finally, see my comment below.
| return [f](const T *x, const double *p) { return (*f)(const_cast<T *>(x), const_cast<double *>(p)); }; | ||
| } | ||
| } else if constexpr (std::is_invocable_v<Func &, const T *, const double *>) { | ||
| return std::move(f); |
There was a problem hiding this comment.
I wonder if we could simplify this by starting the block as follows:
if constexpr (std::is_constructible_v<std::function<Signature>, Func>) {
return {std::forward<Func>(f)};
// Or if moved into the body of the constructor:
fFunc = std::function<Signature>{std::forward<Func>(f)};
} else if (...) {Like this, we delegate everything we can immediately to the std::function constructor. The other cases will probably only have to deal with the constness if I understand correctly.
ParamFunctorwas still carrying the hand-rolled type erasure that the otherROOT::Mathfunctors got rid of in 6c68bbd and a24465f: aParamFunctionBaseinterface,ParamFunctorHandlerandParamMemFunHandlerimplementations of it, threeFuncEvaluatorpartial specialisations to tell pointer types apart, a manualClone(), a raw owningImpl *with hand-written copy constructor, assignment operator and destructor, and about 40 lines of commented-out code.All of that is what
std::functiondoes, and the class already had astd::functionconstructor sitting next to it. Store a singlestd::function<T(const T *, const double *)>instead and let the compiler generate the copy operations.The three callable shapes the
FuncEvaluatorspecialisations used to dispatch on are kept by normalising them in oneAdapt()helper: a callable taking const pointers is stored as is, a callable insisting on non-const pointers (the classicT (T *x, double *p)signature) gets them cast for it, and a pointer to a callable object is called through without taking ownership of it.Constructing and calling a
ParamFunctoris unchanged. The removedGetImpl()andSetFunction()were only handles on the deletedParamFunctionBaseand had no callers.The
<iostream>include went away with the code that needed it; two files that were picking it up transitively viaTF1.hnow include it themselves.🤖 Done with the help of AI