4 ms·
This doesn't make much sense. Sure you can forward declare a struct and functions that operate on it, but you can't call the function or instantiate the struct
by Maxatar 2y ago
This doesn't make much sense.
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.
- iainmerrick 2y agoI'm thinking of this kind of declaration in C: typedef struct foo Foo; extern Foo* foo_create(); extern const char* foo_getStringOrSomething(Foo* foo); 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?
- Maxatar 2y agoIn C++ if you want an opaque type similar to your C example, then you give your class a static constructor. class Foo { public: static std::unique_ptr<Foo> create(); std::string getStringOrSomething(); private: struct FooImp; Foo() = delete; Foo(const Foo&) = delete; auto& self() { return static_cast<FooImp&>(*this); } }; And then you use inheritance to hide your implementation in a single translation unit/ie. a .cpp file source file. struct Foo::FooImp : Foo { std::string something; }; std::string Foo::getStringOrSomething() { return self().something; } With this, there is a single indirection to access the object just as in the C example. But PIMPL is used when you want to preserve value semantics, things like a copy constructor, move semantics, assignment operations, RAII, etc...
- iainmerrick 2y agoThat’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...!
- Maxatar 2y agoThe boilerplate gives you additional type safety compared to C. If you want the same type safety as C, which is basically none, then you can write it as: // foo.hpp struct Foo { static Foo* make(); void method(); }; // foo.cpp struct FooImp : Foo { std::string something; }; Foo* Foo::make() { return new FooImp(); } void Foo::something() { ... } 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.
- 2y ago