Hacker News new | past | comments | ask | show | jobs | submit login

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.




I'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?


In 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...


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...!


The 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.


I don't think it's fair to say that C has no type safety. To recap, I had:

  typedef struct foo Foo;

  extern Foo* foo_create();

  extern const char* foo_getStringOrSomething(Foo* foo);
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.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: