Skip to content

fix(cpp): a call of a member template with explicit template arguments has the type the template returns - #2556

Merged
colbymchenry merged 1 commit into
mainfrom
claude/cpp-member-template-returns
Oct 14, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/cpp-member-template-returns

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

An auto local initialized from a call of a C++ member template with explicit template arguments got no type:

  • protocolbuffers/protobuf: const auto& rep = reflection->GetRaw<RepeatedPtrField<CPPTYPE>>(message, field); and auto* lhs = r->MutableRaw<InlinedStringField>(…);
  • facebook/rocksdb: auto bbto = factory->GetOptions<BlockBasedTableOptions>() and auto del_factory = factory->CheckedCast<CompactOnDeletionCollectorFactory>().

There were two reasons:

  • cppExpressionType read a member call only when its callee ended in the method's name, so the explicit <…> made it no call at all.
  • These templates are declared to return their own template parameter: template <typename Type> const Type& GetRaw(…), T* GetOptions(), T* CheckedCast(). Read as written, that return type is template-dependent.

Calls on such locals fell to name matching: rep.size() guessed SooRep::size, and lhs_string->InternalSwap(…) reached MicroString or ArenaStringPtr by luck.

This was found while working on #2555. protobuf's case is the second shape that chip named, reflection_visit_fields.h:142.

What changed

All in name-matcher.ts:

  • cppExpressionType: a member call may write explicit template arguments (recv->method<Args>(…)). Such a call only reaches template overloads; a non-template Widget* Find() beside template <typename T> T* Find() is never what it calls.
  • cppMemberTemplateReturn: when every template overload is declared to return the same one of its own parameters, perhaps const, a pointer or a reference, the call returns the argument for it, so qualified. It is read as written at the call. Otherwise the return type is read as written, as before. That still serves a project class template like RepeatedFieldRef<T> or Ptr<T>.
  • cppOwnTemplateParameters: reads a function template's own template <…> header, in order and without defaults. A template's node starts at its declaration in both engines, so the header is the code right before it.
  • cppStatementFrom: a macro body's \-spliced lines are read as one statement.

Validation

Site-keyed edge diffs of main e03ff673 against this branch. The base arm is the integration build #2554 and #2555 were merged from, whose tree equals e03ff673; both arms were built from one tree, kernel included.

repo removed added retargeted metadata-only
google/leveldb 0 0 0 0
fmtlib/fmt 0 0 0 0
protocolbuffers/protobuf 3 5 12 1
facebook/rocksdb 1 6 0 22
redis/redis (C, control) 0 0 0 0

Node and file counts are identical. Every change was triaged against the initializer in the source, and all 50 are right.

protobuf:

  • SwapFieldHelper (generated_message_reflection.cc), 17 sites. Each receiver is a MutableRaw<T>(…) local, and its calls now reach T's methods:
    • lhs_cord → RepeatedField<absl::Cord>: InternalSwap, Swap (added);
    • lhs_string → RepeatedPtrFieldBase, InlinedStringField and MicroString in turn (retargeted from MicroString/RepeatedPtrFieldStringAccessor guesses at 0.65). In SwapStringField it had reached ArenaStringPtr at 0.9, through an earlier function's ArenaStringPtr* lhs_string;
    • lhs_map → MapFieldBase (retargeted from UntypedMapBase; Swap from 0.65 to 0.9);
    • lhs_rm → RepeatedPtrFieldBase (added).
  • reflection_visit_fields.h, 2 sites. rep.size() reaches RepeatedPtrField::size, and map.size() reaches MapFieldBase::size. They had reached SooRep and java::FieldGeneratorMap. This is the macro case, read with its \ splices.
  • arena_unittest.cc. outside.get() on auto outside = arena.Make<int>(), a Ptr<T>, reaches Arena::Ptr::get.
  • Removed (3). rep_str.size() on GetRepeatedFieldRef<std::string>(…), a RepeatedFieldRef<T> whose size its specializations declare, no longer guesses SooRep::size.

rocksdb:

  • Metadata-only (22). CheckedCast<T>() locals (del_factory, bfp, collector_factory, merge_operator, adapter) keep their targets, now found at 0.9 instead of 0.65–0.7.
  • Added (6).
    • comp->Inner() on env->CheckedCast<CompositeEnvWrapper>();
    • config->GetOption(…) on opt_info->AsRawPointer<Configurable>(…);
    • the two ChanglingMergeOperator / ChanglingCompactionFilterFactory Name() calls on two-line CheckedCast initializers;
    • ttl_mo->CheckedCast<…>() on a CheckedCast<TtlMergeOperator>() local.
  • Removed (1). shared->get() on GetOptions<std::shared_ptr<Configurable>>(…), a pointer to a std::shared_ptr, no longer guesses SharedCleanablePtr::get.

Tests

__tests__/cpp-member-template-returns.test.ts, 3 tests:

  • protobuf's const auto& rep = over a \-spliced reflection->GetRaw<RepeatedPtrField<int>>(…) reaches RepeatedPtrField::size, with an out-of-line GetRaw definition and a namesake SooRep::size present. The extractor doesn't parse #define bodies, so a fixture can't use protobuf's macro itself; the A/B above covers that site.
  • rocksdb's auto bbto = factory->GetOptions<BlockBasedTableOptions>() against its const and non-const overloads.
  • Overload guards:
    • Find<Gadget>() beside a non-template Widget* Find();
    • Make<Gadget>() with a defaulted template <typename T = Widget>;
    • Get<Widget, Gadget>(1) against overloads returning different parameters, left unguessed.

Mutation check (each guard broken alone; every one fails a test):

guard fails
a member call may write explicit template arguments all three
explicit template arguments call only a template Find<Gadget>()
a parameter return type takes the argument all three
every overload returns the same parameter Get<Widget, Gadget>(1)
a defaulted parameter is read by its name Make<Gadget>()
the header right before the node all three
a macro body's lines are spliced at their \ the GetRaw local

The template keyword check in the header reader recognizes syntax; no output depends on it alone.

One existing test changed: cpp-auto-local-receiver.test.ts, "an initializer that goes on past its line is read to its end". It expected ChanglingMergeOperator::SetName at 0.7, reached by a fallback because CheckedCast<T>()'s T* couldn't be read. It now gets the same target at 0.9, from the type CheckedCast<test::ChanglingMergeOperator>() returns, as the A/B did at options_test.cc:3960.

Full suite: 7,062 passed, 82 skipped. The one failure in the first run was that changed test, at its old 0.7, and it passes with its new expectation.

🤖 Generated with Claude Code

…s has the type the template returns

A member call was read only when its callee ended in the method's name, so
`reflection->GetRaw<RepeatedPtrField<T>>(…)` and
`factory->GetOptions<BlockBasedTableOptions>()` gave the `auto` locals they
initialize no type. Such a call now reaches the class's template overloads,
and when each is declared to return the same one of its own template
parameters (`template <typename Type> const Type& GetRaw(…)`), it returns the
call's argument for it. A macro body's `\`-spliced lines are read as one
statement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 1753472 into main Oct 14, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-member-template-returns branch October 14, 2026 07:59
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.

1 participant