C++ Logo


Advanced search

Re: [std-proposals] Every variable is volatile, everything is laundered, no optimisation

From: Sebastian Wittmeier <wittmeier_at_[hidden]>
Date: Sat, 26 Aug 2023 20:31:30 +0200
Frederick,   That is the famous problem of using (multiple or sometimes single) inheritance for the code sharing hierarchicy and for the interface hierarchy at the same time.   Are IABCComHandler and IXYZComHandler compatible? Do they have the same layout, the same class invariants? Or at least for the usage (member function calls) of this modified pointer?   There probably is no help by any standard extension for those cases. You have lots of binary code, which cannot be changed, and want to modify some pointers to fix a bug in the binary code.   Luckily in this case modifying the pointer worked. In similar cases you would have to resort to patching the binary as the pointer could be accessed in both ways.   You will always have to check manually, whether patches work on existing binaries.   The ABI gives guarantees at certain (e.g. exported by DLL or static library) function boundaries and how the object parameters are represented in memory.     If the hacky static cast of pointers between different types should be made less UB, you should directly use the appropriate calls like launder, start lifetime, ... or write assembler blocks. I do not think there is a one-size-fits-all make this valid and legal solution.   Have you written a non-UB-solution, which should be easier to write or is something missing for writing a non-UB solution, which should be possible in this case?   As you cannot change the binaries at the customer, you also cannot put __verbose onto the function with the dynamic_cast.   -----Ursprüngliche Nachricht----- Von:Jason McKesson via Std-Proposals <std-proposals_at_[hidden]> Gesendet:Sa 26.08.2023 15:55 Betreff:Re: [std-proposals] Every variable is volatile, everything is laundered, no optimisation An:std-proposals_at_[hidden]; CC:Jason McKesson <jmckesson_at_[hidden]>; On Sat, Aug 26, 2023 at 6:31 AM Frederick Virchanza Gotham via Std-Proposals <std-proposals_at_[hidden]> wrote: > > > I'm top-posting here to give an intro here before I reply to comments in series below. > > C++ is a programming language used in the real world by real programmers getting work done. Sometimes we find ourselves in some difficult situations because of a previous bug, or because of a deliberate improvisation to avoid an ABI break. > > I'll give you an example from code I wrote just yesterday. I program the microcontrollers inside microscopes for a multinational firm (microcontrollers made by Texas Instruments (CHAR_BIT==16) and Arduino). > > A microscope that was designed about 20 years ago was based on a communication system called 'ABC', and a new microscope designed three years ago is based on the 'XYZ' communication system. > > There is an extensive SDK for the desktop PC software to interact with the microscopes. For every class beginning with IABC, there is another beginning with IXYZ. > > In order to save a lot of time and effort, the class for the new microscope was made to inherit from the old microscope: > >     class NewMicroscope : Old Microscope {}; > > This has been working fine for 3 years but yesterday I spent a few hours trying to figure out why the new microscope can't manually manipulate the COM port. After a few hours I realised that 'NewMicroscope' inherits from a class that inherits from a class that inherits from a class that inherits from 'IABCComHandler'. The problem here is that it should have instead inherited from 'IXYZComHandler'. So the following always yielded a nullptr: > >         dynamic_cast<IXYZComHander*>(&new_microscope) > > But if I were to change the class hierarchy to make this dynamic_cast possible, then NewMicroscope would no longer inherit from OldMicroscope, but more importantly it would be an ABI break, and I couldn't send out a new DLL file to every customer in a dozen countries just because one customer wants to manually manipulate the COM port (it's a rare request). > > So what did I do? In Visual Studio I wrote: > >     NewMicroscope obj; >     constexpr void *p1 = &obj; >     constexpr void *p2 = static_cast<IABCComHandler*>(&obj); > > Then I just hovered my mouse over the third line and it came up with a tooltip that said '&obj + 16'. Then I found another function in the API that gave back a pointer to another base class whose offset was '&obj + 8'. So I knew that if I could get a pointer to the other class, then I just had to add 8 to it to get the COM port handler. So I sent the customer code that looks like: > >     NewMicroscope obj; > >     IXYZInterface &inter = obj.GetInterface(); > >     IXYZComHandler &com = *static_cast<IXYZComHandler*>(static_cast<void*>(static_cast<char*>(static_cast<void*>(&inter)) + 8u)); > > The code was tested and working before I sent it to them. Is it Ideal to be sending code like this out to customers? No it's not. But I live in the real world. > > If C++ is a real world language then it should have a few features in it that allow 'repair jobs' like this. Sure I could ask my compiler vendor to make a change, but isn't the C++ Standard all about making these feature ubiquitous? That is a lucid, well-thought out motivation, using a real world case study as its foundation. There's just one problem though: the feature you're asking for won't actually address this. See, the key problem in your code is not the pointer arithmetic or the lack of `std::launder` or whatever. The inescapable problem is this: there is no `IXYZComHandler` subobject to be found anywhere within the `NewMicroscope` object. So it doesn't matter what pointer arithmetic and `std::launder`ing you do; the pointer you eventually come up with does not point to an extant object of that type. And that is undefined behavior. There is no combination of `volatile`, `launder`ing, or any other currently existing C++ language features that will make your code have well-defined behavior. The only feature that could fix this is something that would transform an `IABCComHandler` object instance into an `IXYZComHandler` object instance. `start_lifetime_as` could do it, but only if these types were implicit lifetime types (and I rather suspect that they aren't). And even then, I'm not sure what the ramifications are of invoking these functions on base class subobjects. The thing you actually seem to be wanting is some kind of fenced code block that just turns off the C++ object model entirely, replacing it with... well, something else. Because here's the thing: the C++ object model is what defines what happens when you do stuff with object in C++. So once you turn off the C++ object model, it is entirely unclear how to interpret the meaning of *any* code or what its expected behavior ought to be. Let's take your code as an example. You effectively convert a pointer to an `IABCComHandler` to a pointer to an `IXYZComHandler`. Now let's say you invoke a member function on it. OK: what should that *do*? What does it mean to access an object that doesn't exist? What does it mean to call a member function on an object that isn't there? The standard has no answers to these questions. All of these start with something the standard considers to be out-of-bounds, so it *cannot* say what ought to happen. What you're looking for is a new object model, a fundamentally different way of interpreting what the literal text of the code means. Without such an object model, there's no way to say what your code should do. -- Std-Proposals mailing list Std-Proposals_at_[hidden] https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2023-08-26 18:31:32