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 bysizeof(unsigned long)plus any alignment padding.- Stack-allocated
tableinstances 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:
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.