Why should I have written ZeroMQ in C, not C++ (2012)
250bpm.com
250bpm.com
For example: make all constructors private and empty and use public static factory methods for manufacturing new instances. Now the factory can fail, and return an error to boot.
Similarly, ditch C++ exceptions and introduce the notion of a status object that methods return. You always have the option of C-style semantics without dumping the genuine advantages C++ brings.
In terms of some of what is brought up in part II, a C++ engineer can (and would) implement linked-lists C-style. See: http://www.boost.org/doc/libs/1_55_0/doc/html/intrusive/list...
The weird thing about C++—and the reason it didn't just absorb/replace C—is that it gives you the ability to encode strictly more nonsensical things than C does. In that sense, C looks more like the descendant of C++ than the other way around!
Yet the most famous current C compilers are all written in C++.
If it wasn't the rise of open source and its community attachment to C, mainly in GNU and BSD circles, C would have been long replaced except for the embedded space.
Perhaps less in what features to avoid, but there's plenty of hidden gotchas to avoid in JavaScript still.
https://www.youtube.com/watch?v=et8xNAc2ic8 https://dorey.github.io/JavaScript-Equality-Table/
> C++ is a huge language, javascript is a relativly small one.
C++'s ISO/IEC 14882:2003 weighs in at 786 pages, compared to ECMA-262 (5.1 Edition)'s 258 pages (measuring by PDF reader). A ~3x difference, but not quite the order of magnitude I'd expect from huge vs small.
That's not meant to be snarky, but what's is the minimum size of a language spec? I don't know the answer, but I'd think the delta over that would be the better comparison. For example if you can't really spec a language in less than 200 pages then C++'s spec is more like 10 times as big as JavaScript.
> Subtract and branch if not equal to zero[edit] > The SBNZ a,b,c,d instruction ("Subtract and Branch if Not equal to Zero") subtracts the contents at address a from the contents at address b, stores the result at address c, and then, if the result is not 0, transfers control to address d (if the result is equal zero, execution proceeds to the next instruction in sequence).
This is all of it.
EDIT: Said Wikipedia page was sufficiently detailed for me to implement a Brainfuck -> MSIL compiler without additional external references, which I was able to verify worked with a Mandelbrot renderer.
TL;DR: don't trust anyone who tries to use the length of a specification as a comparable proxy for complexity. I can write a 10,000-page standard for Scheme if you hire me to.
How would you prefer to compare language sizes? Gut feel? Surely that's even worse.
I'll certainly admit that - like LOC measurements - it's a metric one can't take too seriously on it's own. On the other hand, it's a useful starting point for discussing said variables.
For example, when I was eyeballing the Javascript standard (I've already read the C++ standard a decent bit), I saw it (like C++'s) defines a grammar, and includes some core library bits (e.g. the Object type.) On the other hand, I didn't see the full browser DOM API you'd use when writing most Javascript - but then again, you don't see any modern graphics or UI widget APIs in the C++ standard either.
This was enough to convince me that they were comparable enough for an orders-of-magnitude comparison, at least. My gut reaction was that C++'s standard might be more detailed - but that'd be biased in my favor, further undermining the argument that Javascript is significantly smaller than C++.
> I can write a 10,000-page standard for Scheme if you hire me to.
But can you find a 10,000 page standard, for any language? Eventually everyone hits a limit to what they consider worthwhile to write, short of perverse incentives.
> How would you prefer to compare language sizes? Gut
> feel? Surely that's even worse.
Why is it necessary to compare "language sizes" at all? Nobody has ever demonstrated that the "size" of a language (by any definition) has any measurable effect on the quality of the software written in that language. It's as pointless as the classic vim vs. emacs argument.It seems relevant in the context of how difficult it is to master a given language, which spawned this thread of debate. Do you disagree that language size is any sort of barrier?
Again, only one metric, to be taken with a measure of salt. But would you prefer we compare languages based on gut feel? Or is comparing languages unnecessary as well?
> It's as pointless as the classic vim vs. emacs argument.
Understanding the merits of vim vs. emacs informs you which you might prefer. The answer to that argument, over which is "ultimately better" is certainly pointless. You can simply choose whichever works best for you, especially as you can change your mind mostly at will.
But people don't simply have the choice of "choose whichever works best for you" when it comes to programming languages on a team project. Nor are languages as fungible as editors when it comes to changing your mind - switching languages mid-project is a massive time sink, and frequently fatal to the project.
Including the standard library.
I don't know about JavaScript, but at least Python, Java and C# are actually quite more when including the standard library.
I've done both and hunting things down in chrome's dev tools is orders of magnitude easier than figuring out what is going on in your c++.
PHP has absolutely awful parts for example. (Magic Quotes anyone?) Even relatively "clean" languages like Java or C# can be completely mucked around if you abuse reflection or do dumb things (ie: have property getters / setters in C# do something insane... like get {return blah++;} )
I mean, any language that has mutexes (ie: all of them) can more or less lead to absolutely dangerous situations. C++ RAII may not be the best (compared to D or Rust or whatever), but C++ is the most widespread language that has complete RAII support.
If you even learn the most basic of C++ patterns: shared_ptr<>, RAII destructors... (which are useful concepts in C++'s "replacements" anyway), C++ becomes not only a manageable language... but one of the most widespread useful languages current right now.
""" A generic library should be able to calculate the distance: for any point class or struct, not on just this mypoint type in more than two dimensions for other coordinate systems, e.g. over the earth or on a sphere between a point and a line or between other geometry combinations in higher precision than double avoiding the square root: often we don't want to do that because it is a relatively expensive function, and for comparing distances it is not necessary """
I suspect a little bit of complexity is necessary. (And can you even build something like this in a more fashionable language?)
* undecidable grammar: http://yosefk.com/c++fqa/defective.html#defect-2
* const inside containers: http://yosefk.com/c++fqa/const.html#fqa-18.1
* template error messages http://yosefk.com/c++fqa/templates.html#fqa-35.17
C is often nicer than C++ because it's simpler: it only has the C pitfalls not the combination of C & C++ pitfalls that plague C++.
Here's idiomatic hello world, with <iostream> vs. <stdio.h>
> g++ -E hello.cc | wc -l
17906
> gcc -E hello.c | wc -l
842
If you really believe that, then you're probably writing dangerous code and not even realizing it.
Most C++ pitfalls come from subtle interactions between features that don't even exist in C, like exceptions, move semantics, templates, threading, constructors/destructors, references, operator overloading, etc. Those are the things people are talking about when they mention the pitfalls of C++.
The things you mention are the pitfalls of C, which technically still exist in C++, but they're not as interesting there because the C++ pitfalls are so subtle and dangerous.
In fact "threading" in C++ is safer because of the availability of high-quality libraries providing task-based parallelism such as TBB, move semantics is used all the time in C albeit manually with pointer manipulation. It is true that not all of the features play well together, but it's nothing compared to the C pitfalls that I mentioned.
If you want to avoid dangerous interactions, don't overengineer your code, stick to the features you understand. This is true not only for C++, but for any language.
Well, if I use libraries to avoid C's pitfalls, that language will be safe, too.
> If you want to avoid dangerous interactions, don't overengineer your code, stick to the features you understand. This is true not only for C++, but for any language.
That's almost useless advice. You might as well say the key to using C++ is to use Python or C instead.
"Stick to the features you understand," doesn't even really work when coding by yourself. C++ is so incredibly complicated there's a very good chance most people wouldn't even really understand the limited subset they've stuck to. C++ devs with years of experience routinely mess up stuff like exactly when copy and move constructors are called.
For example, how many copy constructors get called in this code:
const std::string myString{"omg"};
std::cout << myString << (myString + myString);
Most people will have to think about it for a little bit, and a lot of them will answer wrong.And that advice completely goes out the window when working with code written by other people. And it also means not using any C++ libraries because a lot of them use the full features of C++ and expect (or require) the code calling them use those features, too.
The key to using C++ is to be aware there are many pitfalls, know when you're working with features that are particularly dangerous, have some resources that can help identify and avoid them (like the Effective C++ books), and whenever possible have other people review your code.
I'm not talking about possibilities, but about libraries that are actually available and widely used.
> You might as well say the key to using C++ is to use Python or C instead.
And that might not be a bad idea. If you don't need the power of C++ or don't want to invest time in learning it, then you are better off using a simpler language such as Python, but probably not C for the reasons I pointed out earlier.
C++ is a complicated language, so I agree with your advice of using good learning resources and code review. I don't agree that the issues you mentioned are of the same level as the ones I was talking about. Missing a copy constructor is just not as big of a problem as, say, getting a segfault caused by misuse of varargs.
Then your original statement makes no sense. The C pitfalls you listed are easily avoided using C++ builtins like std::shared_ptr, templates, the STL, and the rest of the C++ standard library.
> I don't agree that the issues you mentioned are of the same level as the ones I was talking about. Missing a copy constructor is just not as big of a problem as, say, getting a segfault caused by misuse of varargs.
And I'd have to strongly disagree. Some of the pitfalls in C++ are worse than crashing because they can (potentially) silently corrupt data. Pass around pointers or references to objects in an inheritance hierarchy without paying attention, and it's not hard to get memory "slicing" and data corruption. Not understanding the intricacies of std::atomic can cause insanely hard to reproduce deadlocks. And I could go on all day.
At the end of the day, I just think it's flat out wrong to say that most of the pitfalls in C++ come from C and are easily avoided. The C pitfalls are easy enough to avoid, but the new C++ specific ones are just as bad, and usually more difficult to understand and spot in real code.
That was exactly my point.
> And I'd have to strongly disagree.
OK, let's agree to disagree.
Just switching compilers makes the C code more robust.
And they certainly beat C's "equivalent", macros.
That's what wxWidgets did since forever, and to the dismay of C++ purists, I find their code easier to read than most templates.
Not to mention it compiles much faster.
Let's take one specific example -- connecting to a remote host. The loop that goes through the DNS returned IP numbers uses local error codes to try each in turn until one is accepted. If none are accepted it throws an exception so that the higher level application code can decide what to do about the connect failure. The exception ensures that all resources are properly cleaned on the way through.
The exception is used to guarantee that resources are properly de-allocated on the way back up to where the error needs to handled as it cannot be handled locally, probably not in the function that kicked off the connect attempt either, but there's no local exceptions to handle the case where the error is handled locally.
The use of the exception is more like a ROLLBACK on a database transaction together with error reporting -- it helps ensure correctness by backing out properly changes in state that were kicked off by the connect attempt. When the exception is caught the program state is exactly as it was when the connect attempt was first tried so we know we have good clean state to make another attempt or to move on and do something else. Backing out all of these other state changes is really hard when an error code needs to be handled non-locally -- and it's this non-local handling of errors that's so hard and error prone without exceptions.
So if you're only ever using error codes you'll far too easily make mistakes when the error needs to be handled non-locally, and if you're only ever using exceptions then it's going to be real ugly when the error is handled locally.
You need to use both if you want clean code that is going to work reliably (or you need massive engineering resources to fix all of the bugs you'll end up with).
E.g you have function called connect() then it should return something like a state object with a connection instance or an error indicator such as {err:dns_fail, dns_servers:[..],hostname:".."}
Otoh, if you were writing a one-of script for scraping a web site then you don't care about fault tolerance and then connection failures are exceptional events so throwing on them becomes ok.
If you can handle the connect failure locally (i.e. probably no further away then caller of the connect function) then by all means do so. If you find another structure in your code makes other things simpler, but no longer allows you locality of error handling you should use an exception.
Well designed libraries allow you to choose. Check out Boost.ASIO for an example of this. Every API has both an exception and error handling version and you're free to use whichever is most appropriate for what you're trying to achieve.
This whole thing about 'expected' and 'unexpected' errors is a red herring that leads people down the wrong path.
The exception backs you out of the transaction that your code is performing -- this is the way to think about it. An error code allows you to try something else whilst keeping the transaction alive. Which you use depends not at all on whether you think the error is "normal" or not.
I once worked with a billing system that threw a BillingException when a charge didn't go through. It works ok, as long as your only response to such an error is to spew an error message to the user. But the more you need to handle it, the less an exception makes sense. The code that tried to handle the BillingException was a tangled mess of try and catch statemetns mixed with retry loops. For example, if there was a temporary error at the payment provider, you would just retry a few times, a permanent error, try with another account, if the customers remaining funds were to low show an error message or if they were just a few dollars of, charge them that and be happy we got something from them.
On the other hand, the billing system could also throw a DbException in case there was something wrong with the database connection. That's a different kind of problem and fatal for a database-backed website. Nothing to do, except log the error and crash.
The point of exceptions is not so much that you can catch and handle them, but that it separates exceptional situations from in-band normal error handling. Now what is in-band and what is exceptional depends on the circumstances which is why, as you say, Boost.ASIO provides both variants. If your system is supposed to handle the errors, use the one without exceptions. If not then use the one with exceptions.
Of course if you redefine "exceptional" to mean out of band error handling then we're in perfect agreement :)
I've also written billing systems and the worst possible thing that happens is that you get stuck on a single customer due to some unforeseen circumstances and can't go on to bill others. The ability to abort the in-code transaction for that customer and move on to the next is exactly what you want from the system, and the exception there makes that a far simpler thing to do (together with RAII for clean up).
> function that both returns values and throws exceptions is more complex
Indeed, but it doesn't really matter because the clean up you get from exceptions with RAII means that the correctness of the function is easier to reason about and that's what you really care about.
In C (my preference over C++) I would use an arena memory allocation strategy in this case. This is something I learned from reading the python interpreter source code: when your parser barfs way down in the call stack, an arena is a simple way to clean up everything. (I'm not saying it will be easy in your particular case.)
As for exceptions, you can just use the return value to signal an exception, or if you want to be a bit more radical, use a longjmp.
You can't bypass all of this in all cases assuming that you only need to deallocate memory. Destructors tidying this up as the exception unwinds the stack is extremely easy to reason about and stops any number of idiotic mistakes.
The stated design goal is that the project "never fail and never exhibit undefined behaviour" yet exceptions are exactly for undefined behavior. If you can't allocate memory, if your function is called with arguments that it cannot possibly interpret, or that a fully expected network connection has failed and cannot recover. Those are exceptions. For all your defined behaviors, you can control just as described in the article without exceptions.
There is clearly too many exceptions (and handlers) in the first case and way too few in the second case.
http://yosefk.com/c++fqa/exceptions.html#fqa-17.1
http://yosefk.com/c++fqa/ctors.html#fqa-10.17
http://yosefk.com/c++fqa/exceptions.html#fqa-17.3
The FQA helped my crystallize a lot of the reasons I hated C++. Even if you like C++, it's a good idea to understand the FQA's arguments, for the same reason people play devil's advocate.
I don't detest the language the way he does, but the FQA really resonated with me. There's no one feature that kills the language, but it seems like everywhere you go you run into a feature that feels like it was designed to trip you up, and it gets tiring after a while.
For example, exceptions, as decribed in the article. Exceptions themselves make sense in some scope: they model some cases of error handling and controlled longjmping really well while are totally unsuited for other cases where a more fine-grained control of failure is necessary. Thus, exceptions themselves are applicable to an extent in a certain scope.
However, in C++, the scope where exceptions actually do work is even more narrow than the scope where exceptions would generally fit. Like explained in the article, the FQA, and this thread, there are numerous pitfalls where exceptions totally break and a number of assumptions you have to make but can't guarantee. This reduces down to a few "known almost safe ways" of using them with a lot of "ifs" bundled in, and you need to explicitly know those ways.
Unlike in C, in C++ you can't deduce what works and what doesn't out of merely reading or writing code: you have to have enough experience to know these pitfalls first hand so that you know how to avoid them and why. There's basically a line drawn in sand of what is known to work and what is not, like medical remedies in ancient cultures.
And further, all this effort is only really needed to deal with the language itself. It's not required to solve the programming challenge at hand. It's something that is not needed with C because you can reason with C on a "what I see is what I have" basis.
I guess you have not yet seen too much UB or compiler specific semantics then.
( If you dont’t understand why, dont’t worry it will be obvious to you in 3 years :-)
Not to mention Go having multiple return values IMHO also takes away the major pain point of returning errors in C; using up your only return value and having to take pointer arguments when you would rather not.
#ifdef _WIN32
# ifdef EXPORTING
# define DECL __declspec(dllexport)
# define STORAGE
# else
# define DECL __declspec(dllimport)
# define STORAGE extern
# endif
#else
# define DECL
# define STORAGE
#endif
// For every return tuple type actually used:
STORAGE template class DECL std::tuple<error_type, return_type1>;
STORAGE template class DECL std::tuple<error_type, return_type2>;
STORAGE template class DECL std::tuple<error_type, return_type3, return_type4>;
// etc.
Even without the above, exposing STL across shared objects leads to the games of "which side of the shared object did this get created" and "which STL did this shared object link against". Losing these games leads to sizeable butt pain.This alone makes exporting STL objects pretty much impossible in a sizable cross-platform project, in my experience.
It's only the workaround (tuples) of this C++ issue that platform-specific issues are allowed to arise, like Windows as you observed, which is why I view the platform-specific issues as symptoms of a deeper problem.
> if multiple return types were natively supported by the C++ language, none of this would be needed since no one would be trying to export a STL object across shared object (or DLL) boundaries.
I believe you're talking about C++ export. Everybody will say that export was too complicated to implement; although (1) the full sentence should be "too complicated to implement, using current linkers," and (2) both CFront and EDG implemented did implement export using current linkers CFront implemented it in the early '90s), so the argument that it can't be implemented is simply wrong. It is accurate to say "using current linkers, refusing to use nonportable linker features, and following the Borland template instantiation model ( https://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.ht... ), it is hard to implement export; only EDG ever paid the price to do so."
But the C macros to #declare dllimport/dllexport (which is what I thought your original complaint was) are meant to solve a Windows linking issue that plagues C as much as it plagues C++. And, to be honest, it's a link time optimization that non-Microsoft linkers don't appear to have trouble with.
i.e.,
int foo_new(foo_t **res);
int foo_mutate_copy(foo_t *foo, bar_t *bar, foo_t **new_foo);
Instead of foo_t *foo_new(); // returns NULL on failure
foo_t *foo_mutate_copy(foo_t *foo, bar_t *bar); // returns NULL on failure
I also like the Go way, just thought this way of doing things in C should be mentioned in the thread.I specifically mention the C way of doing things- but use you demonstrate it above. I was suggesting the Go way is superior.
Also, with Go's approach you have to either explicitly ignore the error or deal with it.
If I'm doing something wrong here I'd love to know it.
- you can collect errors in a function and return them once or provide an extra method, like http://golang.org/pkg/bufio/#Scanner.Err
- you can type switch on the error; maybe there are errors that you can recover/retry locally, such as connection errors, or general io failures - so you do not need to pass back all errors to the caller
More on errors: https://blog.golang.org/errors-are-values
I do find littering code with err checks can be annoying and ended up reaching a compromise. For all code that is likely to be part of an externally public package I use 'if err !=' checks. For application level code that will be internal to my application (and internal to the file I'm working in) I use a small library [1] I wrote, that uses the panic-defer-recover style talked about in the Go documents for internal error handling [2].
However, I find that in most situations I usually prefer to not use panic-defer-recover, with if checks providing more clarity.
[1] https://github.com/surullabs/fault [2] http://golang.org/doc/effective_go.html#recover
I feel like so many developers have been so damaged by Java's approach to exception handling that, of course, error codes seem simpler by comparison. I can't help by feel the recent push-back against exceptions is a step backwards in many situations. A whole generation of developers will have to re-learn the pain that lead to the development of exceptions in the first place.
I don't like that I can't locally hide the boiler plate. There's obviously some measure of essential tension there; I could cope.
I still haven't gotten over the use of product types for return values rather than sum types, and I'm not sure if I will.
But Exceptional C++ made me not want to deal with C++ exceptions, ever. If getting C++ code correct in the face of exceptions is that bloody hard, then the language is broken. I'm convinced that cobbling up an "int" equivalent type that is exception safe is a pyrrhic endeavor that will be a continual source of fragility in production code.
Alexandrescu's Modern C++ Design made me want to forget that templates ever existed. The entire second half of that book I kept saying to myself, "If I'm on a project with someone actually checking stuff like that in, one of us is going to have to leave." Fortunately I've yet to run into those kinds of template metaprogramming games in the wild. Good programmers seem to abhor them.
[Edit: Actually, I remember a bunch of stuff in Modern COM that used templates very heavily. Essentially impervious to debugging; that stuff sucked hard]
More recent C++ design decisions are better than the ones the committees made in the 90s and early 2000s. But the successful teams I've seen all practice restraint and keep things debuggable by limiting the amount of magic going on under the hood, regardless of what the committees have been pushing.
The Google C++ standards (and others that were independently created and haven't been seen much outside their own development cultures) may be "dated", but they remain an indictment of the design of C++.
Blame C, as they are that way to keep C++ exception behaviors compatible with C semantics.
First I can think of: don't create situations where error can happen. Compile with exceptions turned off. Out of memory? Terminate the process/thread.
Another: create your objects using some factory, make constructor empty and initialize class by said factory once instance is already created (perhaps recursively instantiating its members). This way all error generating code will be moved outside of constructor.
See how Google approach it: https://google-styleguide.googlecode.com/svn/trunk/cppguide....
I thought it was routine. Until C++11 made smart pointers standard, it was unreasonably difficult to write exception-safe code, so my understanding was that a lot of C++03 code didn’t even try. Game developers would routinely use -fno-exceptions and -fno-rtti for reliability and performance reasons.
EDIT: actually, there are two defects here. One are the implicit numeric conversions. The other one, which causes me most grievance in this context, is that size() on containers returns an unsigned type.
Now, you often want to do arithmetic with numbers, right? You could think that, given two containers with c1.size() < c2.size(), that c1.size()-1 < c2.size()-1, right? Wrong.
Because of default promotion rules and size() returning an unsigned number, (signed) 1 is converted to an unsigned number and the whole expression is evaluated in modulo arithmetic.
Now, if c1 is empty (c1.size() == 0), then subtracting 1 from 0 in unsigned arithmetic wraps around (this is how unsigned arithmetic is defined) to a huge number (4 billion on 32 bit machines). The simple comparison above will fail in THE singular case of having an empty container on left hand side.
You also lose any possibility of detecting errors. (E.g., if size() were signed and could return a negative number, you could effectively assert on that. If assert triggered, then something really bad happened -- maybe two concurrent threads modifying the data structure w/o proper synchronization.)
I personally think it makes reasoning about a program harder; the above comparison was just the most trivial example. unsigned<>signed comparisons also trigger a bunch of warnings, because I use signed numbers for everything else, so the code ends up having a bunch of unnecessary casts. (I do not want to disable the warning because genuine mishaps of mixing signed/unsigned do happen.)
I believe the consensus is (also Stroustrup's recommendation) is that unsigned should be used as "give me modulo arithmetic" instead of "this number cannot be negative".
So, IMHO, unsigned size() was a mistake and is a big PITA when you try to develop robust software and creates less maintainable code (e.g. in the above simple example, you'd have to special-case the c1.empty() case with if). Casting size() to signed type immediately if I'm going to do anything else than iterate over the container is the least evil for me.
Except that
typedef int bool; /* or BOOL */
#define FALSE 0
#define TRUE (!(FALSE))
used to be quite common back then. So C++ only adopted an existing idiom in the C community.Besides, C++ broke nevertheless broke C compatibility because ++bool will never increment the value past 1, and bool=int will coerce int to 0 or 1. C will preserve the original values.
Yes. I have seen enterprise code making interesting tricks to covert handle values into pointers, back in the late 90's.
On a related note, I'd like to see the results of it being coded in a modern Ada or SPARK with one of the best compilers. I'd be interested in seeing it with full runtime checks on and off with conservative optimization. ZeroMQ with strong implementation assurance could lay the foundation for all kinds of secure networking and middleware schemes. Heck, I'd like to see a team formally verify a version of it with an executable for common architectures. I'd have fun with that. :)
Go or Rust? They might not quite match your dream outlined above, but I think in practice they are pretty much used with the purpose of achieving something similar. They both can be "unsafe" as needed and integrate well with C and ASM.
For that niche it's quite nice, and helps avoid refcounting errors which can be very subtle in C, but Vala is still a fairly leaky abstraction over the C to which it compiles.
So, it's not a dream so much as me wanting to see an excellent middleware implemented in a language proven for decades to do what the two you referenced hope to do someday. Using proven methods, even if ugly or unpopular, is just what engineers do when solving new problems. I'd happily use the same thing built in Rust (not Go as it's less safe) if they can get it more compact and fast. Meanwhile, I have to use C++ and C implementations because that's what's available.
Can you expand on this? I have had no issue getting to C levels of performance by using "unsafe" or ASM a few small critical sections as needed (tight bodies of inner loops, etc or mmap based allocation).
These would be a nice start on assessing it as a true C++ alternative for varieties of systems programming. Come to think of it, I might also look to see if any C++ developers in high-end gaming have tried it. They're so tight on efficiency (and skilled at getting it) that might tell me plenty.
I believe that Objective C is a pretty good (yes, it's getting old) combination of C compatibility with object oriented features as well. The 2 big downsides IMO are: (1) essentially restricted to Apple ecosystem and (2) the syntax keeps a lot of developers away.
The message-passing syntax is such a funny thing to me; it looks weird, but it takes about 1 minute to grok if you try, and then the rest is very simple. It's really dynamic and lets you do some very powerful things that you wouldn't expect to be able to do in a C-derived language (NSProxy, respondsToSelector:, etc.).
I find it very natural when doing ObjC development to write all my high-level code is very "objecty" style, and then call into pure C methods when I'm doing something tricky/low-level/fiddly.
On the downside, though, mostly due to it being old and not very well adopted, ObjC never got a proper generics system, which is really annoying if you're used to Java or C++.
Since C++ and ObjC are both (essentially) supersets of C, you can always mix however much (or little) of plain C that you want. Everything about OO in ObjC is very dynamic. It allows you to do some very cool/flexible/unsafe/unwise things.
In my opinion, that where the tradeoffs start showing up. Flexibility, runtime speed, nice abstractions, development speed, code complexity, etc. Really difficult, if not impossible, to have them all at once.
Despite the unusual looking OO syntax of ObjC, overall it's a pretty simple language given the power it provides. Once you get acclimated to looking at its syntax, I'll even dare say that it's much easier to read and understand than many other languages.
So, it's not that people are stupid or inferior to me. They've just put more of their time in other things (eg work, coding) rather than digging out gems stored behind paywalls or published in obscure places. I've been digging for years along with R&D on much of it. I share that stuff where it might help and it's helped many people/companies in the past. So, I keep doing it.
People are free to ignore any suggestions. About 99.999% do.
Rust [1], basically!
I appreciate you bringing it back to my attention. It's worth someone evaluating thoroughly against C and C++ for systems programming (incl OS) in terms of performance and maintenance. Not sure if I can squeeze time in but I'll suggest that to others at least.
It exists already, only needs more libraries and more users.
Yes, they are removing the GC dependency from the standard library.
"Why I should have written nanomsg in Rust, not C".
From what I have read so far, implementing intrusive lists is not easy in Rust. (If you know of a working intrusive doubly linked list implementation in Rust 1.0 that does not invoke undefined behavior, please tell me; I'd like to know how it should be done.)
Here's what I found after a Google search, by one of the core Rust devs (unsurprisingly).
It can be used in a freestanding (nostd) environment such as a kernel. It uses unsafe code but provides a safe interface. The primary technique is to embed the type that is iterated over in a larger struct which contains the links. This way I can give out references to the inner type without fear of invalidating the iterator.
I see. I was too fixated in embedding a small struct with the links within the larger struct (in the style of the Linux kernel "struct list_head"), and didn't think of doing the opposite.
It probably makes being in several intrusive structs at the same time much more complicated, but I think it's possible to do without breaking anything.