Skip to content

fix(cpp): of two overloads alike but for a const, like protobuf's DynamicCastMessage(const MessageLite*) and DynamicCastMessage(MessageLite*), a call reaches the one C++ picks - #2618

Merged
colbymchenry merged 1 commit into
mainfrom
claude/cpp-overload-constness
Oct 16, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/cpp-overload-constness

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

Follow-up from #2616, filed as a task chip there.

The bug. Overload fit by argument type scored a const T* parameter and its T* twin alike, and the one declared first won. Protobuf's DynamicCastMessage<T>(ptr.get()) on a std::shared_ptr<Message> took const T* DynamicCastMessage(const MessageLite*), where C++ picks T* DynamicCastMessage(MessageLite*).

The fix.

  • cppConstTwinChoice (new). Of the best-fitting overload's twins, alike but for the const of a pointer or reference parameter, it takes the one that adds const to the fewest arguments. It is a tie-break among twins only: an earlier version penalized every const T& parameter, and that moved fmt's to_string(bigint) off its only viable const T& overload.

  • cppWrittenTypeFit's hard rules. These all score -3:

    • a const object given to a pointer or reference to a non-const type;
    • a value a call returns given to a non-const reference (cppNamesObject tells a named object from a call's result);
    • an object given to a pointer;
    • a pointer to characters given to a pointer to another type, and the reverse.

    Two exceptions:

    • an argument of a template parameter's type is exempt from all of them (fmt's const S& fmt could be a pointer);
    • a parameter the function's own template parameter types (Src* x) is exempt from the pointer and const rules, because it deduces the const (rocksdb's static_cast_with_check).
  • Argument types.

  • Template headers are read up to 8 lines above a declaration, not 3. fmt's join spends four lines on its parameters, so its S&& sep wasn't recognized as dependent.

A/B on the C/C++ kit (main 0893606e vs this branch)

repo retargeted
leveldb, redis 0
fmt 14
rocksdb 25
protobuf 194

No call was added or removed. Every retarget was compared against C++'s choice, overload pair by overload pair:

  • Const twins. protobuf's DynamicCastMessage/DownCastMessage move to the pointer and reference twins matching their argument's constness (48 calls), including the static_cast<const MessageLite*>(from) forwarders. RefAt(base, …) on a const void* base takes const T& RefAt(const void*, …) (16), and VisitDescriptors(file, proto, …) on a mutable proto takes the mutable overload (5). In rocksdb, GetOffset, GetOffsetAs and AsRawPointer on a void* take their non-const twins (12). The GetExtensionSet(m) calls on a const m take its const overload (3).
  • Objects off pointer overloads.
    • protobuf: WithVars(EnumVars(…)) reaches WithVars(Map&&), not const Map* (110), plus GetMiniTable(message) on a PtrOrRaw<T> (3) and MicroString(*new auto(…)) (1).
    • fmt: the appender/iterator arguments of format_decimal, format_base2e, fill_n and write_significand, and print(wos, …) on a std::wostringstream.
    • rocksdb: toku_unsafe_fetch(m_sto_txnid) → T&, and GetAndRefSuperVersion(cf) on a ColumnFamilyId → uint32_t.
  • Arrays as pointers. fmt's Char buffer[…] and char xdigits[…] arguments now reach the Char* overloads. rocksdb's Add(level, sst, smallest, largest, …) on constexpr char smallest[] reaches the const char* overload. protobuf's python generator's printer_->Print(m, template) reaches Print(const Map&, string_view), because a constexpr char template[] no longer counts against string_view.
  • Others. rocksdb's MultiGet(ReadOptions(), keys.size(), …) reaches the num_keys overload.

Intermediate arms caught three regressions before this version:

  • the const T& penalty that moved to_string(bigint);
  • the template-parameter cases: rocksdb's static_cast_with_check(&other) moved to its shared_ptr overload, and fmt's to_string_view(sep) was a coin flip;
  • char arrays no longer counting against ColumnFamilyHandle*, which moved rocksdb's db_->Put(opts, key, ts, value).

All are fixed, and the final arm shows none of those changes.

Tests

  • New file __tests__/cpp-overload-constness.test.ts (5 tests, all failing on 0893606e, checked on a detached checkout). It covers protobuf's cast helpers with pointer and reference twins, const and non-const holders, variables, references and a temporary, and class methods with either twin first. It also covers a T* const argument, overloads that differ by more than a const, an array, a deduced Src*, arguments of a template parameter's type (a one-line and a four-line header), char arrays against a class pointer and against a deduced T*, a pointer against const char*, and a constexpr array.
  • The existing overload suites still pass: argument types, choice, nullptr/deleted, free functions, container accessors, member-template returns, specialization names.
  • Mutations: all 19 killed. They cover each rule, the dependent/deduced exemptions, the twin shape, the fewest-added choice, leading const/constexpr, array decay, the holder's template-argument const, both header windows and cppNamesObject.
  • Full suite: 7210 passed. One mcp-status-freshness.test.ts test hit an EBUSY teardown under load; the file passes 4/4 when rerun on its own.

🤖 Generated with Claude Code

…DynamicCastMessage(const MessageLite*)` and `DynamicCastMessage(MessageLite*)`, a call reaches the one C++ picks

Overload fit by argument type (cppArgumentTypeChoice) scored a `const T*`
parameter and its `T*` twin alike, and the one declared first won: protobuf's
`DynamicCastMessage<T>(ptr.get())` on a `std::shared_ptr<Message>` took the
`const MessageLite*` overload.

- cppConstTwinChoice: of the best-fitting overload's twins, alike but for
  the `const` of a pointer or reference parameter, the one that adds `const`
  to the fewest arguments.
- cppWrittenTypeFit: a pointer or reference to a non-const type never takes
  a const object, nor a reference a value a call returns (cppNamesObject
  tells a named object from a call's result); an object is no pointer, nor a
  pointer to characters a pointer to another type (or the reverse). None of
  that for an argument of a template parameter's type, and the pointer and
  `const` rules not for a parameter the function's own template parameter
  types (it deduces the `const`).
- The argument's type keeps a `const` its declaration writes in front
  (`const Buffer* b`, `constexpr char key[]`), which the declarator regex
  leaves out; an array is a pointer to its element; `&ref` is a pointer to
  the referee; cppLibraryElementOf keeps a `const` in a holder's template
  argument (`std::shared_ptr<const Message>`).
- Template headers are read 8 lines up, not 3 (fmt's `join`).

A/B on the cgstd kit against main 0893606: leveldb and redis identical;
fmt 14, rocksdb 25 and protobuf 194 calls retargeted, each triaged against
C++'s choice (const twins, objects and arrays off the wrong pointer
overloads, temporaries to `T&&`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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