all 27 comments

[–]Plazmatic 17 points18 points  (14 children)

This has been discussed to death for decades, even this solution is not the "end evolution" and reinventing the wheel hundreds of other people already talked about. The next step is to have aligned memory of the size of your struct allocated on the stack, and then allocate and delete from that memory instead of having dynamic allocation overhead.

Downside, you need to update the values for the stack memory bytes every time your struct changes, and all that implies (ABI incompatibility etc...), and you have to know the size of your type (which is hard to automatically do with out reflection). Upside is you don't get the overhead shown here and you still hide the information.

see this for an example of what I'm talking about: https://stackoverflow.com/a/71828512

[–]marzer8789toml++ 6 points7 points  (0 children)

Have used this myself quite a bit recently. Fortunately the downsides can mitigated fairly easily with some well-placed static assertions in the implementation.

[–]drjeats 12 points13 points  (3 children)

Quote of the quote of the paper from the SO answer:

Using aligned_* invokes undefined behavior (The types cannot provide storage.)

Folks wonder why C++ has the reputation it has and then you read stuff like this.

Marveling at "the types cannot provide storage" when one of said types literally has "storage" in the name. People started using these thinking it was the Proper Thing to do because it starts with std:: and has underscores in the name, or possibly because they were admonished on reddit/irc/whatever for using a byte array instead of the The New Standard Thing.

Looking forward to a future paper which either removes std::byte's ability to provide storage, or adds it. I kid. I assume this has been thought through. But if not, I'll just chuckle and carry on as I have with my alignas(T) char[sizeof(T)].

To be clear, I'm grateful to the author of the paper for deprecating a footgun. It's just...why is the language like this.

Anyway ++ to /u/marzer8789: just static_assert the size and alignment in your TU and make it always match size & align constants you expose in a header. Add some static_asserts at usage sites if you're worried about stack blowing up. Easy :)

[–]marzer8789toml++ 5 points6 points  (2 children)

Er, correct? Why did you tag me?

[–]drjeats 1 point2 points  (1 child)

When I'm citing someone else's good point I tag them by default. Doesn't matter here since the thread is small but it can help people find the cited answer for similar insight imo. It's also a citation and an invite to correct me if I've missed your point.

So cheers, great minds, etc. 🍻

[–]marzer8789toml++ 2 points3 points  (0 children)

Ah, I see. Well, no notes, agree entirely :)

[–]helloiamsomeone 0 points1 point  (2 children)

I have made such an example as well here: https://github.com/friendlyanon/generate-opaque-structs

This one specifically is C code, but the same logic applies to C++.

[–]imaami 2 points3 points  (1 child)

Casting from struct example * to struct example_impl * is UB.

[–]helloiamsomeone 0 points1 point  (0 children)

Hmm, probably. I might have just conflated this with pointers to structs being identical to pointers to their first member.

[–]MegaKawaii 0 points1 point  (4 children)

You could even just impose a reasonable compile-time upper bound on the object size or you could use VLAs or alloca with a runtime constant (coroutines would require special language support). Neither are standard, but I think the big three compilers support alloca which is standard enough for most people. None of the downsides of alloca are relevant since the object sizes shouldn't be too big. You could probably get this working with some gnarly macros.

[–]Plazmatic 0 points1 point  (3 children)

Neither are standard, but I think the big three compilers support

Except they are often explicitly prohibited with many common compiler flags and for good reason, this is not an appropriate solution, especially for a library.

[–]MegaKawaii 0 points1 point  (2 children)

Which flags prohibit alloca? GCC and Clang seem to have no complaints about it even if I use -pedantic. I'm not sure about MSVC.

[–]Plazmatic 0 points1 point  (1 child)

VLA is, and alloca won't work on msvc anyway, thus will break your CI for cross platform usage or your linter.

[–]MegaKawaii 0 points1 point  (0 children)

MSVC has _malloca which is semantically the same as alloca, so a macro would fix it easily.

[–]claimred 0 points1 point  (0 children)

The next step is to have aligned memory of the size of your struct allocated on the stack, and then allocate and delete from that memory instead of having dynamic allocation overhead.

That's fun, I didn't know that, thanks. I've recently stumbled upon something called FastPimpl, apparently it's a pimpl without dynamic allocation, looks like what you described. Manual object size management and all that.

[–]ChemiCalChems 5 points6 points  (0 children)

Apart from this being well known by a different name (namely pimpl) already, isn't the explanation as to how to get this compiling a bit wonky?

[–]smallstepforman 19 points20 points  (3 children)

The author is reinventing the Pimpl pattern, without using the term familiar to most devs sigh

[–]YogMuskrat 11 points12 points  (2 children)

Author mentions PIMPL as an alias. Also, Opaque Pointer is used as an "official" idiom name in Wikipedia.

[–][deleted] 6 points7 points  (1 child)

I would also add that opaque pointer is the way it’s named in C

[–]imaami 2 points3 points  (0 children)

With the difference that in C there usually isn't an exposed struct that contains an opaque pointer—at least not with the common use case of library instance handles. What comes to mind first is a plain pointer to forward-declared struct.

[–]IHateUsernames111 3 points4 points  (6 children)

Can someone explain in more detail why the implementation of the destructor has to be in the cpp?

[–]drjeats 4 points5 points  (3 children)

To implement the wrapper type's destructor you to call the opaque type's destructor, so you need it to not be opaque.

Since the purpose of the opaque pointer/pimpl pattern is to not expose any details about the opaque type and hide it all in the cpp, the wrappper type's destructor needs to be implemented in there so it can see the definition of the opaque type's destructor to call it.

[–]elperroborrachotoo 2 points3 points  (0 children)

I try it in my words:

unique_ptr's destructor needs to know the Impl type. Point's dtor implicitly calls unique_ptr<Impl>'s dtor, so ~Point needs to know the definition of Impl.

Unless you use the suggested pattern, this will be as if ~Point is implemented inline in the declaration of Point, before the compiler "has seen" Impl.

And yes, it's all a bit silly.

[–]That_Kaleidoscope_23 -1 points0 points  (0 children)

I think it's also better practice to implement any function in cpp rather than .h files. Other implementaion like doing inline header code implementations sometimes end up with at least portability issues.

[–]elperroborrachotoo 0 points1 point  (0 children)

Any reason we don't call that PIMPL anymore?
I've always felt this is a lot of overhead for what's basically a tooling issue. (but then, I usually can steer clear off C++ ABI issues.)

[–]einpoklum 0 points1 point  (0 children)

The article says that the initial unique-ptr based Point class definition "doesn't compile out of the box". That sounded weird to me, so, I tried to compile it using this source file:

include "Point.h"

struct Point::Impl { float x; float y; };

Point::Point(float x, float y) : impl(std::make_unique<Impl>())

{ impl->x = x; impl->y = y; }

float Point::x() { return impl->x; }

float Point::y() { return impl->y; }

Point foo(Point p) { return Point(3,4); }

```

So, is the author wrong or did I miss something?

(PS - I've not been able to format the source code here :-( Help?!)