Skip to content

Case 75: Internal detail:: impl struct embedded by value

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds internal_type_leaks_via_public_api
Source files browse on GitHub

Category: Internal-leak | Verdict: BREAKING

What breaks

namespace mylib::detail { struct table_impl { /* ... */ }; }
class table { detail::table_impl impl_; };   // embedded by value

The public mylib::table class embeds mylib::detail::table_impl by value โ€” no pointer indirection, no pimpl. v2 adds a new layout_kind field to detail::table_impl. Because the impl is embedded by value, every byte of the impl's layout propagates into the public class:

  • sizeof(mylib::table) grows by sizeof(unsigned long) plus any alignment padding.
  • Stack-allocated table instances in caller code overflow their v1-sized slot.
  • Containers of table (std::vector<table>, arrays, etc.) compiled against v1 headers compute the wrong stride for v2 binaries.

The author touched only the "internal" struct โ€” but the binary interface of the public class moved with it.

Why abicheck catches it

The existing struct_field_added detector flags the new field on detail::table_impl. By itself that finding looks like a non-public change. The internal_type_leaks_via_public_api overlay walks the reachability graph from mylib::table (a public exported type), finds that one of its fields has type mylib::detail::table_impl, and surfaces a synthetic finding whose description cites the embedding path:

mylib::table โ†’ field:impl_ โ†’ mylib::detail::table_impl

The overlay also notes that the leak is embedded-by-value, meaning the change propagates the layout โ€” not just the identity โ€” into the public class.

Code diff

// v1
namespace mylib::detail {
struct table_impl {
    unsigned long row_count;
    unsigned long column_count;
};
}

// v2 โ€” one extra field on the "internal" struct
namespace mylib::detail {
struct table_impl {
    unsigned long row_count;
    unsigned long column_count;
    unsigned long layout_kind;   // NEW โ€” shifts mylib::table's size
};
}

How to fix

Hold the impl by pointer instead of by value (pimpl) so the public class's size becomes sizeof(void*) and is decoupled from the impl layout:

class table {
public:
    table();
    ~table();
    unsigned long row_count() const;
private:
    struct impl;          // forward declaration only
    impl* p_;             // fixed size, no layout leakage
};

References

  • Herb Sutter, Exceptional C++ โ€” the canonical pimpl write-up.
  • oneTBB / oneDAL public APIs use pimpl for exactly this reason: the internal detail struct can grow across releases without ABI impact.

Source files

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.