Date: Sat, 17 Feb 2024 15:12:25 -0500
On Sat, Feb 17, 2024 at 12:47 PM Frederick Virchanza Gotham via
Std-Proposals <std-proposals_at_[hidden]> wrote:
>
>
> Let's say version 1.5.6 of my shared library (a DLL file on MS-Windows, or an SO file on Linux), contains the following class:
>
> class IDevice {
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> };
>
> Objects of the type IDevice are only ever created or destroyed inside my library using two exported functions 'CreateDevice' and 'FreeDevice'.
>
> Let's say there's a program called Froggy that was built to link dynamically with v1.5.6 of my library.
>
> Two years go by, and let's say in version 2.3.7 of my library, I make an extension to the IDevice class. I want the old Froggy binary to continue to work properly when at runtime it finds v2.3.7 of my library instead of v1.5.6. That is to say, I don't want an ABI break, and I want backward compatibility. There's two ways of achieving this, the first technique is as follows:
>
> class IDevice {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> };
>
> class IDevice2 : public IDevice {
> public:
> virtual void *GetInputBuffer(void) = 0;
> virtual void *GetOutputBuffer(void) = 0;
> };
>
> All of the preexisting functions that deal with an 'IDevice' can simply dynamic_cast to an 'IDevice2' if they need to access the buffers.
>
> The second technique is to simply extend the original class. The second technique is as follows:
>
> class IDevice {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> virtual void *GetInputBuffer(void) = 0;
> virtual void *GetOutputBuffer(void) = 0;
> };
>
> This won't change the size, alignment or layout of an 'IDevice' object -- it will just extend the vtable by two pointers. This isn't an ABI break.
>
> I've recently taken over coding a big SDK library, and I see both of the above techniques used in the existing SDK code. I prefer the second technique as it's simpler. I'm not even sure why the first technique was ever used -- maybe because there are some cases where the second technique results in an ABI break (e.g. multiple inheritance or virtual inheritance or whatever I don't know).
>
> Now of course the Standard doesn't mention vtables, nor does it allude to something more generic such as a 'polymorphic facilitator', but still I wonder could we possibly standardise the extending of vtables without inducing an ABI break?
>
> Maybe the C++26 standard could have a paragraph something like:
>
> - BEGIN TEXT
> Given two distinct translation units, each containing a definition of a class named S, where the class S is defined differently in the two translation units,
So you want to throw ODR out the window.
Sorry, but no.
> and if we refer to the definition in the first translation unit as S1, and if we refer to the definition in the second translation unit as S2, then any object of type S2 can be safely treated as being an object of type S1, so long as S2 is an exact replica of S1 except for one or more of the following additions:
> (1) A static member function or a static member variable
> (2) A non-static member variable, so long as it comes after all of the member variables shared with S1
> (3) A non-static non-virtual member function
> (4) A non-static virtual member function, so long as it comes after all of the non-static virtual member functions shared with S1.
> (5) A constructor or an overloaded operator, so long as the class's type traits remain intact (e.g. is_nothrow_assignable)
> (6) A typedef or using declaration
> (7) A friend
> - END TEXT
>
> Furthermore, the standard header <type_traits> could provide the following new helper class:
>
> template<class S1, class S2>
> class std::is_abi_compatible {
> inline static constexpr bool value = . . . ;
> };
>
> So then you can ensure at compile-time that you haven't broken the ABI, as follows:
>
> #include <type_traits> // is_abi_compatible
>
> class IDeviceOld {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> };
>
> class IDeviceNew {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> virtual void *GetInputBuffer(void) = 0;
> virtual void *GetOutputBuffer(void) = 0;
> };
>
> static_assert( std::is_abi_compatible_v<IDeviceOld,IDeviceNew> );
>
> typedef IDeviceNew IDevice;
>
>
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Std-Proposals <std-proposals_at_[hidden]> wrote:
>
>
> Let's say version 1.5.6 of my shared library (a DLL file on MS-Windows, or an SO file on Linux), contains the following class:
>
> class IDevice {
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> };
>
> Objects of the type IDevice are only ever created or destroyed inside my library using two exported functions 'CreateDevice' and 'FreeDevice'.
>
> Let's say there's a program called Froggy that was built to link dynamically with v1.5.6 of my library.
>
> Two years go by, and let's say in version 2.3.7 of my library, I make an extension to the IDevice class. I want the old Froggy binary to continue to work properly when at runtime it finds v2.3.7 of my library instead of v1.5.6. That is to say, I don't want an ABI break, and I want backward compatibility. There's two ways of achieving this, the first technique is as follows:
>
> class IDevice {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> };
>
> class IDevice2 : public IDevice {
> public:
> virtual void *GetInputBuffer(void) = 0;
> virtual void *GetOutputBuffer(void) = 0;
> };
>
> All of the preexisting functions that deal with an 'IDevice' can simply dynamic_cast to an 'IDevice2' if they need to access the buffers.
>
> The second technique is to simply extend the original class. The second technique is as follows:
>
> class IDevice {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> virtual void *GetInputBuffer(void) = 0;
> virtual void *GetOutputBuffer(void) = 0;
> };
>
> This won't change the size, alignment or layout of an 'IDevice' object -- it will just extend the vtable by two pointers. This isn't an ABI break.
>
> I've recently taken over coding a big SDK library, and I see both of the above techniques used in the existing SDK code. I prefer the second technique as it's simpler. I'm not even sure why the first technique was ever used -- maybe because there are some cases where the second technique results in an ABI break (e.g. multiple inheritance or virtual inheritance or whatever I don't know).
>
> Now of course the Standard doesn't mention vtables, nor does it allude to something more generic such as a 'polymorphic facilitator', but still I wonder could we possibly standardise the extending of vtables without inducing an ABI break?
>
> Maybe the C++26 standard could have a paragraph something like:
>
> - BEGIN TEXT
> Given two distinct translation units, each containing a definition of a class named S, where the class S is defined differently in the two translation units,
So you want to throw ODR out the window.
Sorry, but no.
> and if we refer to the definition in the first translation unit as S1, and if we refer to the definition in the second translation unit as S2, then any object of type S2 can be safely treated as being an object of type S1, so long as S2 is an exact replica of S1 except for one or more of the following additions:
> (1) A static member function or a static member variable
> (2) A non-static member variable, so long as it comes after all of the member variables shared with S1
> (3) A non-static non-virtual member function
> (4) A non-static virtual member function, so long as it comes after all of the non-static virtual member functions shared with S1.
> (5) A constructor or an overloaded operator, so long as the class's type traits remain intact (e.g. is_nothrow_assignable)
> (6) A typedef or using declaration
> (7) A friend
> - END TEXT
>
> Furthermore, the standard header <type_traits> could provide the following new helper class:
>
> template<class S1, class S2>
> class std::is_abi_compatible {
> inline static constexpr bool value = . . . ;
> };
>
> So then you can ensure at compile-time that you haven't broken the ABI, as follows:
>
> #include <type_traits> // is_abi_compatible
>
> class IDeviceOld {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> };
>
> class IDeviceNew {
> public:
> virtual char const *GetName(void) = 0;
> virtual unsigned GetType(void) = 0;
> virtual void *GetInputBuffer(void) = 0;
> virtual void *GetOutputBuffer(void) = 0;
> };
>
> static_assert( std::is_abi_compatible_v<IDeviceOld,IDeviceNew> );
>
> typedef IDeviceNew IDevice;
>
>
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Received on 2024-02-17 20:12:37