C++ Logo


Advanced search

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

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Sat, 26 Aug 2023 09:55:13 -0400
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.

Received on 2023-08-26 13:55:25