Skip to content

feat: add function object support#1163

Open
gennaroprota wants to merge 2 commits intocppalliance:developfrom
gennaroprota:feat/function_object_support
Open

feat: add function object support#1163
gennaroprota wants to merge 2 commits intocppalliance:developfrom
gennaroprota:feat/function_object_support

Conversation

@gennaroprota
Copy link
Collaborator

@gennaroprota gennaroprota commented Mar 3, 2026

Edit: This description is now obsolete. Please refer to the commit messages for up to date info.

This PR adds detection of constexpr variables whose type is a record providing only operator() overloads, and documents them as callable objects in the Functions tranche. The type of the variable is hidden as implementation-defined, and breadcrumbs render in f() style for readability, but operator() signatures remain fully visible in the variable synopsis and each overload gets its own detail page---preserving the fact that the callable is a function object, not an ordinary function.

Detection triggers:

  • Auto-detection (on by default, controlled by auto-function-objects) for records whose only public non-special members are operator() overloads, including class templates when the variable lives in an enclosing scope of the type.

  • The @function_object documentation command for cases that fail auto-detection (e.g. types with extra members).

Key implementation details:

  • FunctionObjectFinalizer runs before OverloadsFinalizer: hides the type of the variable, restores operator() to Regular extraction, re-parents overloads under the variable, and appends a fixed disclaimer note.

  • VariableSymbol gains FunctionObjectOverloads and satisfies the SymbolParent concept, so MultiPageVisitor generates detail pages for each operator() overload.

  • Handlebars templates render linked operator() signatures in the variable synopsis with "» more..." links to the detail pages.

Closes #564.

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

🚧 Danger.js checks for MrDocs are experimental; expect some rough edges while we tune the rules.

✨ Highlights

  • 🧪 Existing golden tests changed (behavior likely shifted)

🧾 Changes by Scope

Scope Lines Δ Lines + Lines - Files Δ Files + Files ~ Files ↔ Files -
🥇 Golden Tests 5962 5880 82 20 17 3 - -
🛠️ Source 892 819 73 13 2 11 - -
🧪 Unit Tests 507 507 - 2 2 - - -
📄 Docs 16 16 - 2 - 2 - -
Total 7377 7222 155 37 21 16 - -

Legend: Files + (added), Files ~ (modified), Files ↔ (renamed), Files - (removed)

🔝 Top Files

  • test-files/golden-tests/symbols/variable/function-objects.html (Golden Tests): 1592 lines Δ (+1592 / -0)
  • test-files/golden-tests/symbols/variable/function-objects.xml (Golden Tests): 1355 lines Δ (+1355 / -0)
  • test-files/golden-tests/symbols/variable/function-objects.adoc (Golden Tests): 1303 lines Δ (+1303 / -0)

Generated by 🚫 dangerJS against 9ed3493

@cppalliance-bot
Copy link

cppalliance-bot commented Mar 3, 2026

An automated preview of the documentation is available at https://1163.mrdocs.prtest2.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-03-06 15:47:12 UTC

@codecov
Copy link

codecov bot commented Mar 3, 2026

Codecov Report

❌ Patch coverage is 90.50633% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.69%. Comparing base (a1f9a82) to head (9ed3493).

Files with missing lines Patch % Lines
...ib/Metadata/Finalizers/FunctionObjectFinalizer.cpp 78.40% 8 Missing and 30 partials ⚠️
src/lib/Metadata/Symbol/Function.cpp 94.11% 0 Missing and 3 partials ⚠️
src/lib/AST/ExtractDocComment.cpp 81.81% 0 Missing and 2 partials ⚠️
include/mrdocs/Metadata/DocComment.hpp 0.00% 1 Missing ⚠️
src/lib/Support/Reflection/Reflection.hpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1163      +/-   ##
===========================================
+ Coverage    76.38%   76.69%   +0.31%     
===========================================
  Files          311      314       +3     
  Lines        29672    30117     +445     
  Branches      5863     5949      +86     
===========================================
+ Hits         22664    23098     +434     
+ Misses        4735     4720      -15     
- Partials      2273     2299      +26     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gennaroprota gennaroprota force-pushed the feat/function_object_support branch from 19d7451 to e9b70af Compare March 3, 2026 15:56
@alandefreitas
Copy link
Collaborator

alandefreitas commented Mar 3, 2026

How does this solution relate to the proposals in #564? 🙂

(btw, #564 is from Apr 10, 2024 - it's an investigation that took a long time - not 2026 AI slop 😂)

Edit: nevermind. I think I already got it. :)

Copy link
Collaborator

@alandefreitas alandefreitas left a comment

Choose a reason for hiding this comment

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

Isn't "functors" formal enough to be used here? It would be a much shorter command and a more concise way to describe it over and over again without needing a "_".

The fixtures should go to better directories, though. Maybe the directories need better documentation, but you can see they shouldn't go there by comparing with the siblings (and you'll see it's definitely not "core", which is used for really core functionality that's not optional). Symbol types and their features are tested in the symbols directory for each symbol. And since there's an option involved, every new option should be tested in the options directory. Then we have new features for variables that should be tested. Then we have a new field in variables, and that's not reflected there. Then we have a folder for javadoc commands that should be tested, and we need a test there because we introduced a new command.

The test cases in symbols/variable should also explore what happens when there is non-conflicting type information, but the variable is forced to be treated as a function object anyway. There are real-world patterns like this. Another pattern we see very often is the combination of template parameters and arguments: impl function with template, different variables with the same function but different params, and so on.

In the existing test, I'm not sure I understand the mechanism through which it goes to the list of functions in the interface. I'm not sure if cppalliance does that or shows it as a separate kind of entity. Because they do behave differently. All niebloids also have a special banner describing how that function works differently. Another thing to explore is how all of that works when one is visible, and the other is an implementation detail, and so on. Another thing to test and design is what happens when documentation goes on the other symbol.

In the output of the existing test, all functors look wrong. They look nothing like niebloids in cppreference. Many of them look arbitrary and not like valid C++ syntax at all. For instance, it tends to look like:

bool single_overload();

(not very different from an overload set with the main function name replaced from operator() to the variable name and with const removed).

rather than:

constexpr /* implementation-defined */ single_overload = {};

bool
operator()() const;

One more thing missing is the presentation of this feature on the landing page. The snippets on the landing page are meant to showcase features that only Mrdocs can support, and this is definitely one of them.

Good work here overall, though. This is a big design decision.


/** Whether this variable is a function object.

A function object variable is a constexpr variable whose type
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know I'm nitpicking, but I don't think it has to be constexpr.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, right.

{{~/if~}}
{{~#if name ~}}
{{~#if parent.isFunctionObject ~}}
()
Copy link
Collaborator

Choose a reason for hiding this comment

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

If functions don't have "()" as part of their qualified name, why should function objects have it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, right. I always use "()" after function names, but MrDocs doesn't.

the variable is documented as a callable, and its type is hidden
as implementation-defined.
*/
bool IsFunctionObject = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Mmmm... I often got the recommendation to use a single bool IsFunctionObject in the variable, rather than creating a new symbol type for that. That is valid, but it's not anything but simple. For instance,

  • How do I know if something is only the "impl" of a function object? Does it also get a boolean?
  • And if I don't, how do I remove it from indices in the end?
  • And how do we ensure the generator doesn't generate a page for them?
  • And since function objects have a completely different way of being presented to the user, will we create a new template with ways of presenting them to the user that look more like functions (signature templates, etc), or will we sprinkle a lot of if/else throughout the templates?
  • How do we get the impl object from the variable?
  • If we don't, how do we render information from the impl object in the variable page if we have to? That could happen very often.

I'll probably get some of these answers as I review the PR, but I'm leaving them here so I don't lose context.

auto&
allMembers(VariableSymbol const& T)
{
return T.FunctionObjectOverloads;
Copy link
Collaborator

Choose a reason for hiding this comment

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

So the overloads are both members of the impl object and the variable? Is there a mechanism to ensure visitors don't go through them twice?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. The operator() functions are removed from the record's interface and re-parented under the variable. So visitors won't encounter them twice.

--}}
{{~#if (and parent parent.parent)~}}
{{~> symbol/qualified-name-title parent is-qualified-name-parent=true ~}}{{#if parent.name }}::{{/if}}
{{~> symbol/qualified-name-title parent is-qualified-name-parent=true ~}}{{#if parent.name }}{{~#unless parent.isFunctionObject~}}::{{~/unless~}}{{/if}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's something wrong with this pattern. Functions work without it. My intuition and the PR is forcing this template to do something is was not meant to do. qualified names in the standard don't include ()

// constructor, copy/move assignment operator, or destructor
// ([special]).
bool
isSpecialMemberFunction(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same thing. I just remember doing this somewhere already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Aha. I think you mean the code in DocComment/Function.hpp. I see some issues with that:

  • It doesn't handle default constructors with all-default parameters (only no parameters).

  • It doesn't handle by-value copy assignment.

  • It doesn't check for non-template.

I could fix these issues and re-use it. Should we extract those helpers (which are currently in an unnamed namespace) to a shared location? (BTW, unnamed namespaces in include files are bad! :-))

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes. I think so. Whatever you think is best, though. You understand the problem much better than I do. :)

FunctionObjectFinalizer::
markImplementationDefined(RecordSymbol& R)
{
R.Extraction = ExtractionMode::ImplementationDefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh... This answers one of my questions above. Good strategy :)

markImplementationDefined(RecordSymbol& R)
{
R.Extraction = ExtractionMode::ImplementationDefined;
for (SymbolID const& childId : allMembers(R.Interface))
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about potentially deeper levels?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup, fixed :-).

}

void
merge(VariableSymbol& I, VariableSymbol&& Other)
Copy link
Collaborator

Choose a reason for hiding this comment

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

All these merge functions could use some reflection. That's one of the strongest use cases. I don't know if this should be incremental or in a single issue though.

@gennaroprota gennaroprota force-pushed the feat/function_object_support branch 11 times, most recently from 4c1e267 to fa5731c Compare March 6, 2026 10:41
When MrDocs encounters a variable that is a "function object" (its type
is a record whose only public non-special members are operator()
overloads), it now synthesizes free-function entries named after the
variable. The type is marked implementation-defined and hidden from the
output. Multi-overload function objects are naturally grouped by
OverloadsFinalizer.

For example:

struct abs_fn {
    double operator()(double x) const noexcept;
};
constexpr abs_fn abs = {};

is documented as if abs were a free function: double abs(double x).

Detection:

Auto-detection: records with only operator() overloads and special
member functions (constructors, destructor, assignment operators).
Controlled by the auto-function-objects config option (default: true).

Explicit: @functionobject or @functor doc commands on the variable or on
the type, e.g. for types that have extra public members. Template types:
auto-detected only when the type lives in the same scope or an inner
scope (e.g. detail::) relative to the variable. This avoids false
positives from unrelated types like std::hash<T>.

What's generated:

Each operator() overload becomes a synthetic FunctionSymbol with the
variable's name, parented under the variable's parent scope. For
variable templates, the synthetic function inherits the variable's
template parameters. A "NOTE: This function object does not participate
in ADL..." disclaimer is appended to each synthetic function's doc.
FunctionSymbol::FunctionObjectImpl back-references the implementation
type.

An example of this feature is added to the landing page (the abs example
above).

Closes cppalliance#564.
This moves isSpecialMemberFunction() out of FunctionObjectFinalizer's
unnamed namespace into Function.hpp/cpp as a shared API, and introduces
individual helpers (isDefaultConstructor(), isCopyConstructor(),
isMoveConstructor(), isCopyAssignment(),
isMoveAssignment())---isSpecialMemberFunction() becomes a composition of
these individual helpers.

Contextually, this fixes three bugs in the existing detection logic in
DocComment/Function.hpp:

- Default constructors with all-default parameters were not recognized
  (only empty parameter lists were handled).

- Default constructors with parameter packs were not recognized.

- By-value copy assignment operator=(X) was not recognized.
@gennaroprota gennaroprota force-pushed the feat/function_object_support branch from fa5731c to 9ed3493 Compare March 6, 2026 15:39
Copy link
Collaborator

@alandefreitas alandefreitas left a comment

Choose a reason for hiding this comment

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

Really nice tests. Everything about this PR is really cool 😎😄

constexpr CustomFlagCommand customFlagCommands[] = {
{"functionobject", &DocComment::IsFunctionObject},
// For consistency, the name of the command must be "functionobject"
// and not "function_object; but that's not very readable, so we
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't need to describe what we're not doing. :)

{
// Merge the function-object flag before comparing so
// the flag alone doesn't cause spurious inequality.
I.IsFunctionObject |= other.IsFunctionObject;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to replace these merge functions over time :)

{
if (llvm::StringRef(cmd->Name) == custom.name)
{
jd_.*(custom.flag) = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cool design :)

doc.emplace();
}
doc::ParagraphBlock para;
para.children.push_back(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should do this at the handlebars template level before we get complaints so the user can customize this to do whatever they'd like with it.

name() const;
};

/** @functionobject
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we document that the command has no effect when auto-function-object is true?

operator()() const;
};

/** Variable-level documentation. */
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we document that any comment on the variable or on the struct gets ignored? Or did I get the logic wrong?

template_ctor_fn() = default;

template<class U>
explicit template_ctor_fn(U);
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are many variants of this pattern that use templates at this level. It's a reasonably common pattern to support templates in this case (for instance, all range algorithms). This will probably bite us on capy soon because it's really common.

The other pattern, where the template goes in the struct, is a little rarer and harder to implement. It's also harder to implement because you have to actually instantiate the thing.

It's up to you, but I'd recommend implementing the easier one and opening a "Explore ..." issue for the hard one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

identify niebloids

3 participants