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.
exception_ptr
, and
noexcept
by default
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.
Cases 1-4:
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 libstdc++
.
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++.
exception_ptr
and shared_future
were 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 onexception_ptr
objects shall access and modify only theexception_ptr
objects themselves and not the exceptions they refer to. Use ofrethrow_exception
onexception_ptr
objects that refer to the same exception object shall not introduce a data race. [ Note: ifrethrow_exception
rethrows the same exception object (rather than a copy), concurrent access to that rethrown exception object may introduce a data race. Changes in the number ofexception_ptr
objects that refer to a particular exception do not introduce a data race. —end note ]
noexcept
by 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.
The 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.
When ::operator new()
started throwing std::bad_alloc
, two binary incompatibilities were introduced:
new
no 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.
Adding the 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::ios_failure
."
In C++11 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 u8string
and 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_exception
wasn'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_get
and std::num_put
was proposed for short float,
but it is believed has now been dropped from the proposal.
std::default_order
to associative containers was reverted because it was an ABI break.
The change from lock_guard<T>
to 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)
make 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::size
and 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.)
Same for 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 *::emplace_back
from 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 emplace_back
.
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.
In reply:
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.
Further reply:
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::packaged_task
and 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.