Skip to content

Case 80: Pimpl alias changed from shared_ptr to unique_ptr

Field Value
Verdict 🔴 BREAKING
Category Breaking
Platforms Linux, macOS
Flags ABI break, API break
Detected ChangeKinds internal_type_leaks_via_public_api
Source files examples/case80_pimpl_shared_to_unique/

Category: Pimpl ABI | Verdict: BREAKING

What breaks

oneDAL's pimpl alias is defined as

namespace oneapi::dal::detail {
    template <typename T> using pimpl = std::shared_ptr<T>;
}

…and every public class holds its implementation through that alias:

detail::pimpl<descriptor_impl> impl_;

In v2 the alias is rewritten as using pimpl = std::unique_ptr<T>. On most 64-bit platforms sizeof(shared_ptr<T>) == 16 and sizeof(unique_ptr<T>) == 8, so the containing class even shrinks — the failure isn't "size grew", it is:

  1. Mangled name of every inline accessor changes. Any header-inlined getter that touches impl_ is templated on the pimpl type; its mangled symbol differs between v1 and v2.
  2. Destruction model changes. No control block, no atomic refcount; consumers built against v1 that aliased into the same control block via shared_ptr aliasing constructors now double-free.
  3. Copy semantics flip. v1 was copyable (refcounted share). v2 is move-only. Consumer code that copied a descriptor compiles under v1, refuses under v2.

Real Failure Demo

Severity: BREAKING / ABI SHAPE CHANGE

This smoke app still works because it does not copy or share ownership. The real break is that the public object field changes from shared ownership ABI to unique ownership ABI, changing size, copyability, and inline member code expectations.

cmake -S examples -B /tmp/abicheck-examples-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/abicheck-examples-build --target case80_pimpl_shared_to_unique_app case80_pimpl_shared_to_unique_v2

tmp=$(mktemp -d)
cp /tmp/abicheck-examples-build/case80_pimpl_shared_to_unique/app_v1 "$tmp/"
cp /tmp/abicheck-examples-build/case80_pimpl_shared_to_unique/libv2.so "$tmp/libv1.so"
(cd "$tmp" && LD_LIBRARY_PATH=. ./app_v1)
# class_count = 7 (expect 7)

Why abicheck catches it

The existing type_field_type_changed detector fires on descriptor::impl_ (old type std::shared_ptr<detail::descriptor_impl>, new type std::unique_ptr<detail::descriptor_impl>). The internal_type_leaks_via_public_api overlay (from PR #238) then escalates it because the new field type references the same detail:: namespace that the leak detector already tracks as internal.

Code diff

// v1
template <typename T> using pimpl = std::shared_ptr<T>;

// v2 — silent ownership-model change with cascading consequences
template <typename T> using pimpl = std::unique_ptr<T>;

Why a separate case (not subsumed by case41)

case41 (type_changes) covers generic field-type changes. case80 is the pimpl-shaped instance worth carrying as a named regression: the failure involves three orthogonal axes (mangling, destruction, copyability) and is specifically the kind of change a maintainer might propose during "modernize the API" cleanup without realizing it is binary-incompatible.


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.h
  • v2.cpp
  • v2.h

See also: Examples overview · All BREAKING cases · Category: Breaking.