Hacker News new | ask | show | jobs
by pskocik 3446 days ago
In my codebase, I use expression macros (with a little bit of __typeof__-based type checks) to do it fully generically. Much of what you think you need C++ for can be done almost just as succinctly on top of plain C (I'm talking automated scope cleanups, semi-automated error handling, many generic things) and the compile times fly. I agree C++ has had some good ideas. In fact, I originally wanted to use it. But I came to the conclusion that it's too bloated, and more importantly, fundamentally broken in certain ways (RAII, exceptions, even templates and namespaces), and I ended up emulating what I think the good parts of C++ are on top of plain C. When C++ programmers think C, they usually think lack of generics and lots of explicit manual, micromanagement and pointer arithmetic, but it can be a lot more than that.
4 comments

What is fundamentally broken about RAII? To me that is one of the killer features of C++ over C, which requires manual goto statements to achieve something approaching RAII.
Yes I cannot fathom how you could write successful and maintainable C++ code without using RAII. How can you guarantee safe deterministic lifetimes of objects without using RAII?
By not using exceptions, and putting a delete at the end of the scope where your RAII object would have been cleaned up.
What about early return? In practice this logic becomes much very error-prone.
If you are writing high-reliability C (think military, aerospace, industrial control systems), early return is usually prohibited by coding standards. Then again, dynamic memory allocation usually is too.

Personally, I think early return is lazy programming because not cleaning up non-RAII objects is only one class of bugs that it can introduce. Things like vector renormalization, update notification and even later sections of algorithms can all be missed by early return. Not having early return means you need to explicitly skip later code if you don't want it, rather than essentially turning on "skip-all".

I know three solutions for this:

* (Classic): use goto so that instead of returning directly, all branches jump to a cleanup label. This is tricky to do well, but possible.

* (Nonstandard): GCC offers a nonstandard function attribute to register a function as a destructor for this kind of thing. I'm sure they claim it was inspired by Scheme's dynamic-wind/dynamic-wind concept.

* (Memory pooling): Apache APR relies heavily on memory pools. One feature of APR's memory pools is the ability to register functions to run (in, I believe, non-deterministic order) on data when the pool is eventually destroyed.

Aargh! I should look at nonstandard features that I don't use before I say they solve problems. GCC's construcotrs and destructors (outside of C++ code) are limited to globals and statics. Marking a function a constructor guarantees it's automatically called before main(), and marking a function a destructor guarantees it's automatically called after main() completes.
Naked new and deletes should be seen very little in code post-C++11.

Containers and resource handles are Stroustrup's advice in his blue C++ book.

Manual deletion is something I would never really want to see outside a destructor.

I hope this comment is some sort of weird joke.

What's broken about namespaces? If I create a class named Bitmap whilst using Windows.h, will I conflict with GDI::Bitmap???

RAII - how can you possibly guarantee safe lifetimes of objects without RAII?

Templates - do you like writing the same thing a million times and having to update it and fix it in those million places?

> What's broken about namespaces?

The difficulty I have with them is they are open, not closed. Any piece of code can crack open a namespace and insert more names into it. Thus, you can have problems with "hijacking", where foo(int) is inserted, while unknown to you someone else inserted foo(unsigned) that does something completely different, and the compiler decides the latter is a better overload match.

Complicating things further, the names visible in a namespace are dependent on where the compiler is lexically in the code - more names can be inserted further down.

As a counterpoint, many have suggested to me that this openness is a critical capability they need for their code. My answer is that using namespaces in that manner offers little improvement over just using a prefix on the identifiers.

And so the debate goes on!

Well yes, it is a prefix, but it is a prefix that doesn't need to be used when you are already inside the namespace or if you explicity 'using namespace' it. If I had to type the full namespace of functions and enums all the time I'd go insane.
well yes they are open by design. If you want closed namespaces just put your functions (as statics) inside a struct.

Btw, visibility rules in a namespace (but not in a struct/class) are the same as for the global namespace, so no, a function in a namespace won't see another declared after it.

I'm not familiar with this approach, but I think it would entail code size increase for each use of the macros, and a runtime overhead for the typeof checks, correct?

If so, C++ has a nice advantage in that all the type checks are at compile-time so you don't pay any more for those, and also you don't duplicate binary code with macros.

__typeof_ is a compile-time feature. It's like C++'s decltype. (You can make C++'s auto out of it too.) There's is a potential for code size increases with this approach, that's true. But the potential also exists with inlinable vector methods.
Yes, but for the vector methods, you leave that decision up to the compiler which should be able to take the best route.
True. However, I think in the case of the dynamic array, this effect is minor. The macros are small, and most of the time, they end up wrapped in a function anyway. But, admittedly,it doesn't scale well to very big generics, which should be always wrapped in a function. (Eventually I plan to shove a simple transpiler in front of all my C code and do this, along with namespacing, how I think it should be done.)
Unfortunately __typeof__ is non-standard. Despite its usefulness it always gets left out of the standard (and with weird excuses, I think last time it was lack of implementations - despite the fact that eg. both GCC and LLVM implement it).

Interestingly, C11 defines _Generic selection, a kind of a switch for types, but its usefulness is hindered by the fact that it cannot be used in conjunction with sizeof.

Expression macros `({, })` aren't standard either. But I sort of go with, if all gcc/clang/tinycc support it and it's a highly useful feature, then it's part of C as far as I'm concerned. It would certainly be nice if ({,}),__typeof__, and __label__ became part of the C standard, though.
My Google skills are failing me here. Is `({,})` a special construct, or are the '{' and '}' metacharacters and you're just referring to variadic macros, which are standard as of C11?
It's a common (gcc/clang/tcc) compiler extension (chapter 6.1 of the gcc manual) that allows you to use parentheses around a compound statement to turn the compound statement into an expression whose value is the last statement in the compound statement. For example `({ int _r; if((_r=foo())) bar(); _r })` behaves like an inline function that returns `_r`. If you want to do generic vector ops purely with macros and you want to do it robustly, you effectively need for the macros to "return" a value signalling whether the potential realloc that might have happened inside the macro succeeded or not. It's a very powerful feature because it allows you to have macros that behave like ducktyped, value-returning inline functions. (Sometimes you'll need to tone down the ducktyping a little, like it's probably a good idea to use some __typeof__-based typechecks (http://stackoverflow.com/questions/41250083/typechecking-in-...) to make the compiler warn you if you attempt to memcpy doubles into an int vector from your vector__insert method.)
Thank you! I've never seen this before; looks handy.