Hacker News new | ask | show | jobs
by AnimalMuppet 3066 days ago
Could you give an example of "some basic things are so fundamentally convoluted"?

Also note that almost all C code is legal C++, so when you "just write idiomatic C", you're still writing C++. (Perhaps not idiomatic C++...)

1 comments

A simple case is hiding the implementation and exposing a public API. Let's use objects to make it something C++ ought to be good at and C ought to be bad at. In C, you write a header file adder.h:

    struct Adder;
    struct Adder *adder_create(void);
    void adder_setup(struct Adder *, int, int);
    int adder_operate(struct Adder *);
    void adder_delete(struct Adder *);
You can easily imagine how one would trivially implement these functions in adder.c, allocating an object, mutating its state and deleting it. The caller doesn't know anything about struct Adder nor does it know how the adding of two ints is implemented. It only needs know that struct Adder can be pointed to. The header file defines a clear interface that can be compiled against. The implementation can be changed without having to recompile all callers. Tried-and-true and boring but it works: this is the way C has done it for decades.

Now, C++. You create a class Adder, declare public constructor and destructor as well as setup() and operate(). Then you declare a couple of private ints to hold state and maybe some private helper methods, and then you realise that 1) you've just exposed parts of the private implementation in the public header and 2) you can potentially break compiled binaries even if you only change the private/protected parts of the class. Yes, that's a textbook example of how to define a class that completely sucks for any real-life encapsulation purposes. You see how things are getting complex quick? This is where people began to think of more novel applications of C++ to fix the language itself.

So you define an abstract class IAdder in adder.h with pure virtual methods to act as a truly public interface, and derive an implementation class AdderImpl in adder.cpp. Great. Except you can't instantiate the private implementation. You'll need a public factory function outside the class or a static method such as IAdder::create() to construct an AdderImpl and return it as an IAdder. This isn't very clean and beautiful anymore and this was a simple example. There are more branches to be explored in the solution space but at this point we've basically had to create an ugly reimplementation of something that we thought would come free in a language that namely supports object oriented programming whose one fundamental selling point is easy encapsulation. And all that while the C counterpart is actually easy, understandable and simple, and requires no re-engineering to get it even work.

About your two realizations:

1. I've "exposed" parts of the private implementation in that they were in the header file, yes. They were also labeled "private". That means that someone can read that and gain more information about my implementation then they could from just public information, I suppose. It also means that nobody can actually use them in code, because they're private. So you can think of that as "exposing" if you want, but it's not something that I've ever recognized as any kind of a problem, let alone one worth solving.

2. If you change only the private parts of the class and try to link other code to it without recompiling the other code, yes, you can get a broken compiled binary. That is certainly true. But I could do that in C almost as easily (if everything that uses the struct layout isn't in the same source file). And makefiles that keep track of dependencies aren't exactly rocket science. Neither are clean builds.

Maybe I just don't have your problems, but I'm still not seeing this as much of an issue at all.

Maybe I just don't have your problems, but I'm still not seeing this as much of an issue at all.

Surely problems are always personal, this all did start with the term "pet peeve".

For me, one of the peeves that comes back over and over is indeed that it seems that's near impossible to write a separation between the API and the implementation in C++ that is clean and beautiful. That's one of the first things in any project, drafting out the interfaces. And things get ugly quick there.

There are others but I drafted this particular case as an example that you requested. I don't want to go too deep into the discussion here but I'll reply to the two points below:

1) Exposing fields and functions under the private label is just a matter of principle. It doesn't matter if the private parts cannot technically be used outside the class: it still means there is information in the public API that shouldn't have to be there in the first place. That's just a stupid restriction of quirky language. A header file is mainly about the interface and its documentation: the last thing I want in it is to have cluttering bits of internal crap there only to be skipped over.

I could also cram all the code in a single source file and use global variables because hey, it can be made to work and it's easier to write a linker that way. The same applies with the public interface issue here.

2) Check back my C header file again. The struct layout is only visible to the implementation. As long as the ABI stays the same we can even use a different compiler to build the private implementation into a binary and the interface still works with all existing code.

Yet this is still about the fact that the private bits do not have to be visible to the public, just the implementation. So the language that forces me to do just that for the sake of convenience for the language designer and compiler writers is just plain stupid.

> So the language that forces me to do just that for the sake of convenience for the language designer and compiler writers is just plain stupid.

Nothing prevents you from getting the same ABI hiding in C++, but the users of your code will greatly benefit: no possibility of memory leaks, ownership is enforced since copy & move are disabled, etc:

    /// Adder.hpp ///
    struct Adder {
      public:
        Adder(int, int);
        ~Adder(); 
        
        int operator()();

      private:
        struct Impl;
        std::unique_ptr<Impl> impl;
    };

    /// Adder.cpp ///
    struct Adder::Impl { 
        // your private stuff
    };

    Adder::Adder(int x, int y)
      : impl{std::make_unique<Impl>(x, y)} { 
      
    }

    Adder::~Adder() = default; 

    int Adder::operator()() { 
      return impl->stuff * 2;
    }

    /// main.cpp ///

    int main() {
        Adder adder{1, 2};
        return adder();        
    }

    /// your main.c ///

    int main(int argc, char** argv) {
        struct Adder* adder = adder_create(); // must not forget
        adder_setup(adder, 1, 2); // must not forget
        int res = adder_operate(adder); // note how all lines are longer
        adder_delete(adder); // must delete
        return res; // we had to introduce a specific variable just for the sake of keeping the return value else we wouldn't be able to delete
    }

Of course, your code wouldn't pass code review in either cases: introducing indirections through implementation hiding like this kills performance, in C and C++ likewise. And it's not like hiding your stuff in another object file will prevent anyone from knowing your impl... disassembly & code recreation tools are extremely powerfuls nowadays.
It's convoluted because the idea of forcing an object to always be allocated on the heap is convoluted. The real fix for this problem is the modules system, where the compiled module can expose an object's size without exposing its contents. Your example shows why header files suck more than anything about the core semantics of C++ as a language.
> The real fix for this problem is the modules system, where the compiled module can expose an object's size without exposing its contents.

Exposing the object size is already too much, it's part of the ABI. The truth is : we can't have our cake (maximum perf due to stack allocation) and eat it too (hide all implementation details)

What's your point? Yes, those two goals are in conflict. Modules are still an improvement to the current situation with no downsides.
> What's your point?

My point is that if you want to be entirely safe from the ABI point of view, modules don't help you at all