The first step was to solicit input from EWG/LEWG to try and capture the variety of issues
covered by the single term "ABI breakage".
This document is an informal summary of comments, including many of those made on the two evolution reflectors.
Thank you to all who have provided input; errors and omissions remain, as usual, the fault of the author.
From Michael Wong's original email to EWG/LEWG:
There is a concern that we need to be clear on when we can make clear ABI breakage. There seems to be several cases on a spectrum of Performance vs Stability.
Case 1: NEVER Break ABI leads to slower and slower performance but the best stability
Case 2: we have done that before, but it is unpredictable to users
Case 3: We have never tried this before, the question is what is the appropriate time frame between breakage
Case 4: this is the fastest moving, most performance, and the least stability.
Some implementations have a published ABI - for example on x64 many implementations conform to the Itanium specification (which applies to more than just the Intel Itanium chipset.)
Some implementations publish guarantees about stability of their ABI. For example, Red Hat publish an
"Application Compatibility GUIDE" ; Appendix A guarantees compatability for three releases for various components, including
Such documents and guarantees will not, in general, be under the control of WG21.
Changes to the library can sometimes be accommodated in the library ABI by using sufficiently clever programming to provide backwards compatibility with an existing binary library.
However, the degree to which this is possible, in any given case, can vary between implementors depending on their library implementation. It can be hard to know, without specialist knowledge, how feasible this might be in a specific case.
They didn't have anything on my list that individually felt (even to them) like "we should break ABI for this" - the most impactful bit would probably be improvements to hashing. But they suspect that if we plan for it with enough lead time we'll come up with a lot of quality-of-life and minor performance improvements that add up to a lot.
Or, if we are just going to let "that's an ABI break" be an automatic veto, we should probably update our published priorities. They don't think "ABI stability" is listed anywhere as a "you can rely on this" feature for C++.
shared_futurewere added for C++11 there was an ballot comment raised by GB 74 about the introduction of race conditions where an exception object was accessed by two threads.
The result was wording in C++11 that read (18.8.5 [propagation]/p7)
For purposes of determining the presence of a data race, operations on
exception_ptrobjects shall access and modify only the
exception_ptrobjects themselves and not the exceptions they refer to. Use of
exception_ptrobjects that refer to the same exception object shall not introduce a data race. [ Note: if
rethrow_exceptionrethrows the same exception object (rather than a copy), concurrent access to that rethrown exception object may introduce a data race. Changes in the number of
exception_ptrobjects that refer to a particular exception do not introduce a data race. —end note ]
noexceptby default; this was a breaking change as existing C++03 code that threw an exception in a destructor would terminate if called from C++11 code.
std::string class was changed for C++11, in response to the addition of threads, to make more of its operations safely concurrently executable (which invalidated copy-on-write implementations.) This included changing
data() to require NUL termination.
Additionally, the change was designed to support the 'small string optimisation', which improves performance for strings short enough to take advantage of it.
Implementing this requires an ABI change in general as the size of the class changes, as does the layout and meaning of its members.
gcc provided a dual ABI to support both pre- and post- C++11 code, but it is still the case that by default many installations of gcc still use the pre C++11 implementation.
::operator new() started throwing
std::bad_alloc, two binary incompatibilities were introduced:
newno longer handled out of memory conditions
The changes to the definition of triviality could potentially have changed calling conventions, but to avoid that the Itanium ABI uses the C++98 definition of POD, not the current definition of trivial and standard layout, because that's evolved over time.
std::system_error base class to
std::ios_failure for C++11 was a particularly nasty one for one implementor.
One implementor remarked: "Buy me a beer some time and I'll tell you the story of Schrödinger's Catch, which allows a single catch handler to work for
two distinct types of
std::char_traits changed the parameter types of its members from
const char_type& to passing
char_type by value
(It is believeed this is still not actually implemented in libstdc++).
I think the LWG issues list records quite a few breaks between C++98 and C++03 that probably wouldn't be acceptable today, but back then almost nobody actually implemented the full standard anyway, so making breaking changes was just part of finishing the implementation!
P0482 (char8_t) changed the return type of the
generic_u8string member functions of
std::filesystem::path for C++20.
Whether empty class types as function arguments take up a slot in the argument list or not.
uncaught_exceptionwasn't really an ABI break due to zombie names.
Same for get_unexpected/set_unexpected, etc.
ABI was the reason why we didn't make destructors implicitly virtual in polymorphic classes. "If we can take an ABI break we can fix that."
Note: the ABI was not the sole reason; and the impact of this change would be massive at this stage in the life of the language.
Adding new virtual functions to
std::num_put was proposed for short float,
but it is believed has now been dropped from the proposal.
std::default_orderto associative containers was reverted because it was an ABI break.
The change from
lock_guard was reverted because it was an ABI break.
(Not an ABI break taken, but one that should have been (or should be) taken)
std::unique_ptr<T> be passed as efficiently as
T*. Currently there is
a significant performance and optimization hit from using
std::unique_ptr<T> due to the ABI & calling convention required.
list::sizeand CoW string; there was no change in the specification that would've caused or prevented such a passing convention. There's fairly little we could've done in the standard to impact this.
Numerous aspects of
std::unordered_map's API and ABI force seriously suboptimal implementation strategies.
(See "SwissTable" talks.)
std::map. (For example btree-based sorted containers.)
The most frustrating for one person is
std::vector, which cannot support small-size-optimization due to stability
of pointers & iterators across move.
We changed the return type of *
void to return a reference to the new element.
We didn't do the same for
push_back because that would have broken ABI.
If we could break ABI we could make them consistent and remove one reason to (ab)use
Is that really a problematic ABI break for some compilers? In gcc-land we might stick an abi_tag on it so
the new version gets a different mangling, but I believe the ABI is not
broken. Unless of course you start introspecting and use
decltype(c.push_back(e)), but that's indirect and seems acceptable.
Without the abi-tag the old and new versions of the function have the same mangled name.
One translation unit instantiates the old definition, and in that TU nothing uses the return value (because it's void).
Another translation unit instantiates the new definition, and the caller of the function uses the non-void return values.
You have two instantiations, with the same symbol name. The linker picks one. Because it's a Thursday the linker picks the old definition of the symbol, which doesn't actually return anything. The new TU calls the old symbol, and there is junk on the stack where it expects to find a return value.
Thanks. Sorry, my message was not clear enough. I know all that. My point was that some annotation like abi-tag easily avoids this issue. And you only need a very basic version of abi-tag for that, which should be easy to implement for any compiler that cares about binary compatibility. So I don't think we should refrain from making such changes for ABI reasons.
Since this is a member function, its exact signature is not mandated by the standard, so an implementation could also add an extra argument with a default value, as allowed by [member.functions], to give it a different mangling. But a vendor-specific annotation is more convenient.
Library Fundamentals defines
std::promise with polymorphic allocator members,
which adds a pointer member to the class. That was originally proposed as a change to the standard types when LFTS was enabled,
which would have been an ABI break. Instead the types in LFTS are distinct types in a distinct namespace.
LWG2503 (multiline option should be added to syntax_option_type) is an ABI break.