Case 185: Inherited override reuses the base's vtable slot¶
| Field | Value |
|---|---|
| Verdict | ๐ข COMPATIBLE |
| Category | Addition (Compatible) |
| Platforms | Linux |
| Flags | โ |
Detected ChangeKinds |
func_added |
| Source files | examples/case185_inherited_override_reuses_slot/ |
Category: Addition | Verdict: โ COMPATIBLE (exit 0)
What changes¶
Base::paint(int) is a virtual method. In v1, Derived does not override
it โ calls dispatch straight through to Base::paint(). In v2, Derived
adds int paint(int x) override; with the exact same signature as
Base::paint(int). Derived also keeps its unrelated, non-virtual
helper() method.
Why this is not a new vtable slot¶
A naive "did a new virtual method appear on this class?" scan would see
Derived::paint show up where it didn't exist before and flag it as
VIRTUAL_METHOD_ADDED โ normally a BREAKING finding, since inserting a
slot shifts every subsequent entry in the vtable and desyncs old callers.
But overriding an already-virtual base method with a matching signature
doesn't insert anything โ it replaces the function pointer at the same
slot index Base::paint already occupied. Derived's vtable is the same
size before and after:
vtable for Derived: 40 bytes (5 pointers: offset-to-top, RTTI, ~Derived,
~Derived (deleting), paint) -- unchanged in both versions
abicheck/diff_cxx_rules.py builds old_virtual_signatures() โ a per-class
set of leaf-name(params)cv-ref identities โ from the old snapshot's
class hierarchy, walking transitive bases. When virtual_method_addition()
sees Derived::paint(int) in the new snapshot, it checks whether any base
in Derived's inheritance chain already had a virtual with that exact
signature key. Here Base::paint(int) matches, so the new function is
recognized as an override reusing an inherited slot โ no
VIRTUAL_METHOD_ADDED/vtable-layout finding is emitted for it at all. What
is reported is a plain, additive func_added for the newly-materialized
Derived::paint(int) symbol (it didn't have its own mangled symbol before,
since calls went through Base::paint directly) โ a COMPATIBLE addition,
not a break.
abicheck compare libv1.so libv2.so --header old=v1.hpp --header new=v2.hpp
# verdict: COMPATIBLE (exit 0)
# func_added: New public function: paint
Negative twin: same name, different signature does add a slot¶
If Derived instead declared virtual int paint(double x); โ same method
name, but a signature that does not match any inherited virtual โ there
is no existing slot to reuse. This genuinely grows Derived's vtable by one
entry (40 โ 48 bytes) and is correctly reported as BREAKING:
vtable_slot_count_changed: Vtable for 'Derived' changed size: 40 -> 48 bytes
virtual_method_added: New virtual method added to existing class Derived: paint
See tests/test_kde_compat_detectors.py::test_inherited_override_is_not_virtual_method_added
and ::test_same_name_different_signature_virtual_is_new_slot for the
unit-level pair this compiled example mirrors.
Two detectors, one exemption¶
diff_types.py's _diff_type_vtable() independently compares each class's
list of vtable entries. Left on its own, it would just see Derived's
vtable entry list textually change (Base::paint's slot now names
Derived::paint), even though its length and order are identical, and
unconditionally emit type_vtable_changed โ disagreeing with
virtual_method_addition()'s slot-reuse exemption above and reporting
BREAKING for a case that is genuinely COMPATIBLE.
_diff_type_vtable() calls diff_cxx_rules.vtable_slot_is_override_reuse()
โ the same signature-key comparison virtual_method_addition() uses โ to
recognize when a differing vtable slot is exactly this override-reuse
relationship, and withholds TYPE_VTABLE_CHANGED for it too. The two
detectors now agree:
abicheck compare libv1.so libv2.so --header old=v1.hpp --header new=v2.hpp
# verdict: COMPATIBLE
# func_added: New public function: paint
Reaching that agreement at the castxml layer, on real compiled binaries,
also needed a fix one level lower: dumper_castxml.py's _build_vtable()
previously deduplicated an override against the base slot it reuses only via
castxml's vtable_index attribute. Not every castxml/Clang build emits that
attribute, and without it the reused slot was never deduplicated at all โ
Derived's reconstructed vtable listed both Base::paint and
Derived::paint, one entry longer than the real vtable, which
vtable_slot_is_override_reuse() (a same-length, positional check) cannot
see through. _build_vtable() now falls back to castxml's overrides
attribute (resolved through any multi-level override chain) to collapse the
reused slot in place when vtable_index is absent.
How to reproduce¶
cmake -S examples -B /tmp/abicheck-examples-build
cmake --build /tmp/abicheck-examples-build --target \
case185_inherited_override_reuses_slot_v1 case185_inherited_override_reuses_slot_v2
python3 -m abicheck.cli compare \
/tmp/abicheck-examples-build/case185_inherited_override_reuses_slot/libv1.so \
/tmp/abicheck-examples-build/case185_inherited_override_reuses_slot/libv2.so \
--header old=v1.hpp --header new=v2.hpp --lang c++
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hppv2.cppv2.hpp
See also: Examples overview ยท All COMPATIBLE cases ยท Category: Addition (Compatible).