Skip to content

[math] Use std::function in ROOT::Math::ParamFunctor - #23211

Open
guitargeek wants to merge 1 commit into
root-project:masterfrom
guitargeek:paramfunctor-std-function
Open

[math] Use std::function in ROOT::Math::ParamFunctor#23211
guitargeek wants to merge 1 commit into
root-project:masterfrom
guitargeek:paramfunctor-std-function

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

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.

Constructing and calling a ParamFunctor is unchanged. The removed GetImpl() and SetFunction() were only handles on the deleted ParamFunctionBase and had no callers.

The <iostream> include went away with the code that needed it; two files that were picking it up transitively via TF1.h now include it themselves.

🤖 Done with the help of AI

Comment thread hist/hist/src/TEfficiency.cxx Outdated
Comment thread tutorials/analysis/unfold/testUnfold2.C Outdated
Comment thread math/mathcore/inc/Math/ParamFunctor.h Outdated
typedef T (* FreeFunc ) (T * , double *);
ParamFunctorTempl(FreeFunc f) :
fImpl(new ParamFunctorHandler<ParamFunctorTempl<T>,FreeFunc>(f) )
ParamFunctorTempl(const PtrObj &p, MemFn memFn)

@hageboeck hageboeck Sep 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this declared as a reference if it's supposed to be instantiated with pointers? Have you considered:

template <class Obj, typename MemFn>
   ParamFunctorTempl(const Obj *p, MemFn memFn)

If you allow references here, I could try to instantiate it with a real object, and then it would either break at the capture or when trying to dereference the object for the call, wouldn't it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the reference buys nothing here. Changed to take a pointer:

template <class Obj, typename MemFn>
ParamFunctorTempl(Obj *p, MemFn memFn)

I went with Obj * rather than const Obj *, because const Obj * would make (*p).*memFn a call on a const Obj & and reject non-const member functions, which TF1 is allowing. And since Obj is a template parameter, it can also be deduced to a const type if appropriate.

Comment thread math/mathcore/inc/Math/ParamFunctor.h Outdated
// specialization used in TF1
ParamFunctorTempl(std::function<Signature> f) : fFunc{std::move(f)} {}

T operator()(T *x, double *p) const { return fFunc(x, p); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this one redundant? The const version below can be called also with pointers to non-const objects.

Suggested change
T operator()(T *x, double *p) const { return fFunc(x, p); }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it was redundant. I removed it.

Comment thread math/mathcore/inc/Math/ParamFunctor.h Outdated
return (*fImpl)(x,p);
}
// specialization used in TF1
typedef T (*FreeFunc)(T *, double *);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using FreeFunc = as was used above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This alias is gone now anyway, as a side effect of how your other comment #23211 (comment) was addressed.

Comment thread math/mathcore/inc/Math/ParamFunctor.h Outdated
Comment on lines +58 to +66
/// Construct from any callable object, or from a pointer to one.
template <typename Func, typename = std::enable_if_t<!std::is_same_v<std::decay_t<Func>, ParamFunctorTempl<T>>>>
explicit ParamFunctorTempl(Func f) : fFunc{Adapt(std::move(f))}
{
}

/**
Destructor (no operations)
*/
virtual ~ParamFunctorTempl () {
if (fImpl) delete fImpl;
}

/**
Copy constructor
*/
ParamFunctorTempl(const ParamFunctorTempl & rhs) :
fImpl(nullptr)
{
// if (rhs.fImpl.get() != 0)
// fImpl = std::unique_ptr<Impl>( (rhs.fImpl)->Clone() );
if (rhs.fImpl) fImpl = rhs.fImpl->Clone();
}

/**
Assignment operator
*/
ParamFunctorTempl & operator = (const ParamFunctorTempl & rhs) {
// ParamFunctor copy(rhs);
// swap unique_ptr by hand
// Impl * p = fImpl.release();
// fImpl.reset(copy.fImpl.release());
// copy.fImpl.reset(p);

if(this != &rhs) {
if (fImpl) delete fImpl;
fImpl = nullptr;
if (rhs.fImpl)
fImpl = rhs.fImpl->Clone();
}
return *this;
}

void * GetImpl() { return (void *) fImpl; }


T operator() ( T * x, double * p) {
return (*fImpl)(x,p);
}
// specialization used in TF1
typedef T (*FreeFunc)(T *, double *);
ParamFunctorTempl(FreeFunc f) : fFunc{Adapt(f)} {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these two not be unified into one, using Adapt?

What is the reason for using SFINAE to exclude them being the same type?

@guitargeek guitargeek Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SFINAE was guarding against the by-value ParamFunctorTempl(Func f) taking priority over the copy constructor for a non-const lvalue. But anyway that's not needed anymore, because I have removed the FreeFunc constructor, as it was redundant like you suspected.

By the way, a side note before you also bring up that the std::function constructor is redundant: from the C++ perspective yes, but it has to stay because cppyy depends on that. The callbacks into Python only work via std::function arguments.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 18h 37m 35s ⏱️
 3 861 tests  3 860 ✅ 0 💤 1 ❌
79 602 runs  79 601 ✅ 0 💤 1 ❌

For more details on these failures, see this check.

Results for commit efb4d48.

♻️ This comment has been updated with latest results.

@couet
couet removed their request for review September 3, 2026 08:48
`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
@guitargeek
guitargeek force-pushed the paramfunctor-std-function branch from 3b30a35 to efb4d48 Compare September 3, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants