Is there anyone else that gets unreasonably angry at stuff like PIMPL? It is truly the most braindead, bereft of sense activity in this world. There was a comment in one of the many Rust threads that called C++ a respectable language now but then things like PIMPL snap you right back into the wasteland it is.
PIMPL is an elegant solution to multiple problems. Idk what you could possibly have against it besides the extra work involved. I don't think any language has solved the fundamental problem of hiding details better than PIMPL does.
I really like C#'s `partial` keyword as a solution to the problem of hiding implementation details. It lets you declare a class over several files, so you can have one file which is only the public interface, and another which has private implementation.
That is essentially the same idea as PIMPL. You put the private parts of the class (e.g., the data layout) in some file that is held privately. I guess you could argue that there is extra syntax involved with PIMPL because C++ is more low-level than C#, but it's not so bad. The actual implementation of a class can be spread over as many files as you want in C++.
Yes but pimpl is really just a hack and workaround for the fact that you can't separate the public interface from the implementation details in C++ due to needing to put everything in the class definition. imho `partial` is superior, as you don't need an additional allocation and indirection.
You actually don't need to do that at all. It's common style in C++, but the language does not require it.
With the right techniques, you can absolutely forward-declare basically all of a class's functionality. Then you can put it into its own translation unit.
Members and function signatures have to be declared in the header, but details about member values/initialization and function implementations can absolutely be placed in a single translation unit.
The data layout of a class must be part of its definition. So either you expose the layout (data members), add a layer of indirection with PIMPL, or resort to ugly hacks to otherwise hide the data layout such as having buffers as members. Another possibility is to not use exposed classes for member functions. Then you can just pass pointers around only and never use C++ features. Out of all of these, PIMPL solves the problem the best.
Yes, that's true. But if the concern is build-times, exposing the data layout is harmless. We don't necessarily need full PIMPL just to get improved build times. By keeping the data layout in the .hpp, you can guarantee that your class can still stack-allocate.
if the concern is build-times, exposing the data layout is harmless
Not at all! If your private member variables use any interesting types (and why shouldn't they?) you need to include all the headers for those types too. That's exactly why you get an explosion of header inclusion.
If you change the data layout of your exposed class, you must recompile anything that uses it. That increases build times and also breaks ABI. And as the other guy commented, the data itself has types that also need definitions and file inclusions. Without PIMPL, your data layout can change without touching your header, due to changes in a related header (even a 3rd party header).
You don't need any indirection with partial because in C# all classes already go through a layer of indirection.
However, consider that in C# if you add a field to a struct, which is a value type and hence no indirection, then you do need to recompile all assemblies that make use of that struct. It's no different than C++ in this regard.
And yet C, an even lower-level language, achieves the same effect without the duplication of PIMPL. You just forward-declare a struct, and declare functions that accept pointers to it: the header doesn't need to contain the struct fields, and you don't need to define any wrapper functions. Technically you can do the same in C++. But in C++ to make an idiomatic API you need methods instead of free functions, and you can't declare methods on forward-declared classes. Why not? Well, I can imagine some reasons… but they have more to do with C++'s idiosyncrasies than any fundamental limitation of a low-level language.
The C++ committee could address this, but instead they seem to want to pretend separate compilation doesn't exist. (Why are there no official headers to forward-declare STL types, except for whatever happens to be in <iosfwd>?) Then they complain about how annoying it is to preserve ABI stability for the standard library, blaming the very concept of a stable ABI [1] [2], all while there are simple language tweaks that could make it infinitely more tractable! But now I'm ranting.
First of all, comparing C to C++ in this way is silly, because C++ is a very different language. But there are some similarities.
> You just forward-declare a struct, and declare functions that accept pointers to it: the header doesn't need to contain the struct fields, and you don't need to define any wrapper functions.
Those functions would be more verbose because they must contain an explicit `this` equivalent pointer. This would have to be repeated at every single call site. So it's not really helping.
You don't need wrapper functions for PIMPL. You can have them if you think it's worthwhile, of course.
>Technically you can do the same in C++. But in C++ to make an idiomatic API you need methods instead of free functions, and you can't declare methods on forward-declared classes. Why not?
There are good technical reasons why you can't tack member functions into the interface of a forward-declared function. There would be nowhere for that information to go, if nothing else. I think I heard a talk about adding new metaprogramming features to C++ that might address this in like C++26, but anyway it's not a significant problem to simply avoid the problem
I think you can probably make some template-based thing that would automate implementing the wrappers for you. But it would be a convoluted solution to what I consider a non-problem.
>The C++ committee could address this, but instead they seem to want to pretend separate compilation doesn't exist. (Why are there no official headers to forward-declare STL types, except for whatever happens to be in <iosfwd>?)
Most of the STL types that people need are based on templates. It does not make sense to forward-declare those. I just don't see a use case for forward-declaring much besides io stuff and maybe strings.
>Then they complain about how annoying it is to preserve ABI stability for the standard library, blaming the very concept of a stable ABI [1] [2], all while there are simple language tweaks that could make it infinitely more tractable! But now I'm ranting.
There seems to be a faction of the C++ committee that does not share the traditional commitment to backward compatibility. They have gone so far as to lobby for a rolling release language, which is guaranteed to be a disaster if implemented. I think wanting to break ABI might be a sign of that. Let's hope they use good judgement and not turn the language into an ever-shifting code rot generator.
Keep in mind, there may be ABI breakage coming from your library provider anyway, on top of what the committee wants. So it's not necessarily such a cataclysmic surprise as something you're supposed to plan around anyway. ABI stability between language standards is mostly a concern for people who link code built with different C++ standards (probably, a lot of code). It wouldn't be the end of the world if you had to recompile old code to a newer standard, but it might generate significant work.
> There are good technical reasons why you can't tack member functions into the interface of a forward-declared function.
Are there? You could have a class decorator to mark a definition as incomplete and only allow member functions, types and constant definitions:
// in Foo.h
incomplete struct Foo {
public:
Foo();
Foo(const& Foo);
void frobnicate();
};
// in foo.cc
struct Foo { // redefinition of incomplete structs is allowed
public:
Foo(){...}
Foo(const& Foo) {...}
void frobnicate(){...}
private:
void bar() {...}
int baz;
string bax;
};
edit: there are also very good reasons to fwd declare templates. You might want to add support in your interface for an std template without imposing it to all users of your header. In most companies I have worked, we had technically illegal fwd headers for standard templates.
>Are there? You could have a class decorator to mark a definition as incomplete and only allow member functions, types and constant definitions:
C++ already has a way to do this via inheritance, even multiple inheritance. I suppose some of the same machinery used for inheritance could be repurposed for partial class definitions but it is unnecessary.
Edit: I think I overlooked something here at first glance. Yes it might be nice to have a public partial definition of a class and a private full definition. But the technical reason you can't have this is that using a class in C++ requires knowing its memory characteristics. If that information does not come from the code, then it must come from somewhere else like a binary. Maybe the partial definition could be shorthand for "use PIMPL" but I haven't thought through all the ways it could go wrong, such as with inheritance.
>edit: there are also very good reasons to fwd declare templates. You might want to add support in your interface for an std template without imposing it to all users of your header. In most companies I have worked, we had technically illegal fwd headers for standard templates.
I have never seen illegal forward declaration headers. Not at any company I've worked at, nor in any open-source project. I don't think there is a reasonable value proposition to doing that. What kind of speedup are you expecting from that?
>You might want to add support in your interface for an std template without imposing it to all users of your header.
This sounds good in theory but in practice, most interfaces I've seen use the same handful of types or std headers, so it can't be avoided and furthermore you'd be forcing everyone to bring their own std headers every time (and probably forget why they ever included them in their own code, in the first place). You'd be talking about a lot of trouble to maybe save one simple include, and introducing a lot of potential for unused and noisy includes elsewhere.
Yes, that's why I used the 'incomplete' keyword. Of course you can only pass around pointers and references to incomplete classes (although there might be ways around that).
Base classes almost work, but you either need all your functions to be virtual or you need to play nasty casts in your member functions.
re std fwds, typically the forwarding is needed when specializing traits while metaprogramming.
Ada had all of C++'s problems figured out in 1983. PIMPL as a means of boosting compiler performance is fundamentally braindead. We shouldn't be bending over backwards with broken tools to make them sort of work.
PIMPL doesn't only boost compiler performance. It provides code-hiding and ABI stability for everyone using it effectively. It's like killing 3 birds with one stone. PIMPL for sure isn't gonna be the thing to convince me that C++ is broken.
Ada has piqued my curiosity before but I think if it was as good as you make it sound, it might have at least 1% market share after 40 years. It doesn't. I can't justify the time investment to learn it unless I get a job that demands it.
It's not PIMPL per se that's the problem, it's that C++ needs it but makes it very awkward to write. It feels like the language is fighting against you rather than setting you up for success. At least that's been my angle in this discussion.
Unfortunely "bending over backwards with broken tools" is exactly to what made C++ a success in first place, an idea also adopted by Objective-C and TypeScript.
Trying to make the best out of an improved langauge, while not touching the broken tools of the existing kingdom they were trying to build upon.
Naturally such decision goes both ways, helps gain adoption, and becomes a huge weight to carry around when backwards compatibility matters for staying relevant.
No, maybe the acronym is ugly, sounds like pimple? But other than that the use of the technique is invaluable to writing stable ABIs which in turn makes distribution of C++ libraries a lot easier.
Yes! Why should I need to do extra work, messing up runtime performance by adding a pointer indirection, just to improve compilation time a bit?
In C, idiomatic C, you can forward-declare a struct and the functions that operate on it, and you don't need any indirection at runtime. C++ has plenty of nice features, and in general I'd reach for it rather than C, but for some reason it can't do that!
Sure you can forward declare a struct and functions that operate on it, but you can't call the function or instantiate the struct without the definition. That's no different than in C++.
The purpose of PIMPL is that you can actually call the function with a complete instantiation of the struct in such a way that changes to the struct do not require a rebuild of anything that uses the struct.
It's not about just declaring things, it's about actually being able to use them.
That's a fully opaque type, and it's reasonably efficient. The one thing you can't do store a Foo on the stack, because you don't know its internal size and layout. So it's always a heap pointer, but there's only one level of indirection.
In idiomatic C++ I think you'd have something like:
class Foo {
struct impl;
std::unique_ptr<impl> _impl;
std::string getStringOrSomething();
}
If I have a pointer to a Foo, that's two pointer indirections to get to the opaque _impl. So, okay, I can store my Foo on the stack and then I'm back to one pointer. But if I want shared access from multiple places, I use shared_ptr<Foo>, and then I'm back to two indirections again.
The idiomatic C++ way to avoid those indirections is to declare the implementation inline, but it make it private so it's still opaque. But then you get the exploding compile times that people are complaining about on this thread.
The C approach is a nice balance between convenience, compilation speed and runtime performance. But you can't do this in idiomatic C++! It's an OO approach, but C++'s classes and templates don't help you implement it. C++ gives you RAII, which is very nice and a big advantage over C, but in other respects it just gets in the way.
Edit to add: now I look at this, Foo::getStringOrSomething() will always be called with a pointer (Foo&) so it will always need a double-dereference to access the impl. Unless, again, you inline the definition so the compiler has enough information to flatten that indirection.
I don't see how that pImpl approach can ever be as performant as the basic C approach. Am I missing something?
That’s a nice trick, I don’t recall seeing that one before!
It still seems like an awful lot of boilerplate just to reproduce the C approach, albeit with the addition of method call syntax and scoped destructors.
I feel like there must be an easier way to do it. Hmm, maybe I’m at risk of becoming a Go fan...!
And yes it's rare because nowadays most C++ developers stick as much to value semantics as possible rather than reference semantics, but this approach was very common in the early 2000s, especially when writing Windows COM components.
Nowadays if you want ABI stability, you'd use PIMPL. Qt is probably the biggest library that uses this approach to preserve ABI.
The only valid way to get a Foo (or a Foo ptr) is by calling foo_create(). Inside foo_getStringOrSomething(), the pointer is definitely the correct type unless the caller has done something naughty.
Of course there are a few caveats. First, the Foo could have been deleted, so you have a use-after-free. That's a biggie for sure! Likewise the caller could pass NULL, but that's easily checked at runtime. Those are part of the nature of C, but they're not "no type safety".
You can also cast an arbitrary pointer to Foo*, but that's equally possibly in C++.
A comment like "basically none" should not be taken literally. It is intended to indicate that the difference between the C++ approach and the C approach is that the C++ approach gives you a great deal of type safety to the point that the C approach looks downright error prone.
The C++ approach of sticking to value semantics doesn't involve any of the issues you get working with pointers, like lifetime issues, null pointer checks, invalid casts, forgetting to properly deallocate it, for example you have a foo_create but you didn't provide the corresponding foo_delete. Do I delete it using free, which could potentially lead to memory leaks? The type system gives me no indication of how I am supposed to properly deallocate your foo.
You don't like boilerplate, fair enough it's annoying to write, but is boilerplate in the implementation worse than having to burden every user of your class by prefixing every single function name with foo_?
The C++ approach allows you to treat the class like any other built in type, so you can give it a copy and assignment operator, or give it move semantics.
So no it's not literally true that C has absolutely zero type safety. It is true that compared to the C++ approach it is incredibly error prone.
While older C++ code is rampant with pointers, references, and runtime polymorphism, best practices when writing modern C++ code is to stick to value types, abstain from exposing pointers in your APIs, prefer type-checked parametric polymorphism over object oriented programming.
If anything, the worst parts of C++, including your point about being able to perform an invalid cast, is inherited from C. C++ casts, for example, do not allow arbitrary pointers to be cast to each other.
Does somebody using my "class Foo" really need to know everything about its "std::unordered_map<std::string, std::unique_ptr<NetBeanFactory<RageMixin, DefaultBowelEvictionPolicy>>>" data member?
No, they need know only the size and alignment of "class Foo". Unfortunately, in C++, either a client has to see every type definition recursively down to the primitives, or you give them an opaque pointer and hide all of the internals in the implementation (all they know about "sizeof(Foo)" is "it contains a pointer to something on the heap, probably").
edit: Ok, there's also the copy constructors and other possibly auto-generated value semantic operations, but pretend you've defined those explicitly, too.
I know what pimpl is - I relearn it before every job change for interviews. But I've never seen it in use, and don't see a use for it. Compile times are rarely an issue in my experience. At least not big enough to warrant something that extreme.
I like to see things simple: I need something, so I include something. Compile times are really orthogonal to that, and mostly a job for compiler devs, hardware people, modules or whatever. Changing my code because of compile times seems pretty harsh
pimpl is not for improving compile times (although it can help with that). It's for maintaining ABI compatibility by keeping your implementation details out of public headers.