This actually sounds like an argument for why he should've used a smaller subset (or different) of C++.
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.
And so I will continue to avoid C++ because I'm not a master and will not avoid the many pitfals, and I want to get things done, not become a master of C++.
Actually avoiding C++ pitfalls is not that difficult as most of them come from C: manual memory management, macros, varargs, unsafe casts. In fact C++ provides safe alternatives to many of the unsafe C constructs, so if you stick to them you are fine.
These are trivial issues compared to major safety and usability flaws inherited from C that I mentioned. BTW the C grammar is not context-free either. The compilation speed is mostly due to header system which is again a C heritage, not because the C++ grammar is undecidable. And template error messages are much better now in modern compilers such as Clang. As for the const, its semantics is the same as in C.
The super-slow compilation speed is mostly because C++ encourages massive all-inline template libraries like the standard C++ library and boost, which need to be parsed and have the used templates instantiated for every compilation unit.
Here's idiomatic hello world, with <iostream> vs. <stdio.h>
> Actually avoiding C++ pitfalls is not that difficult as most of them come from C
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.
I don't always write dangerous code, but when I do I fully realize it =). And it has nothing to do with any of the features that you listed.
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.
> 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.
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:
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.
> Well, if I use libraries to avoid C's pitfalls, that language will be safe, too.
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.
> I'm not talking about possibilities, but about libraries that are actually available and widely used.
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.
I've seen some pretty impossible to comprehend or debug uses of templates. Perhaps my favorite, which represents most of the pitfalls you can create with C++ is the Boost library. And of that, the shining star is this page: http://www.boost.org/doc/libs/1_57_0/libs/geometry/doc/html/...
I wouldn't call it a pitfall. Boost is written by C++ experts who like to push the boundaries of what can be done with the language. This doesn't mean that you have to debug their code or overuse templates yourself and there is nothing in the language that encourage this. For common cases such as in the standard library, templates are pretty easy to use and trivial to debug with modern compilers.
"""
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?)
What a strange language, that so much sage wisdom involves what parts of it you shouldn't use. (Former C++ programmer, a long time ago, before the "don't use these parts" advice became a thing.)
There was a post here recently, explaining type systems are effectively about restricting you from encoding nonsensical constructions. C is ASM with the ability to encode some nonsensical ASM instruction-sequences removed, etc.
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!
> 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.
No, actually, it doesn't. C++ is a huge language, javascript is a relativly small one. I know it's funny to compare the size of "Javascript the good parts" with "Javascript the definitive guide", but that's can be attributed to writing style more than percentage of language covered.
> 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.
"Number of pages in specification" is a rather useless metric to use to compare languages, since there are so many variables. And not just things like point size or margins or what have you, but the contents themselves vary. Some specifications include an EBNF grammar, others may try to describe it via natural-language rules, and others may just say "see the reference implementation". Some specifications include documentation on the language's standard library (the scope of which is a factor that itself varies greatly from language to language), and some of these will go into differing amounts of detail (e.g. providing upper bounds for computational complexity of algorithms). Some languages will go into great detail about memory models (especially low-level ones), while others will wave it away as an implementation detail.
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.
> "Number of pages in specification" is a rather useless metric to use to compare languages, since there are so many variables.
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.
> Why is it necessary to compare "language sizes" at all?
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.
The scheme R5RS standard is 50 pages, and is considered quite small for any real language. The Haskell 2010 Language Report is 239 pages. The Forth ANSI standard is 220 pages and is a seemingly simple language.
I imagine Brainfuck could be reasonably specced in around 5-10 pages, based on the size of the Wikipedia description.
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.
Here is one of the smallest useful (ie. turing complete) language's full specification :
> 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).
But most languages that have seen prominent use have obscure corners that no one should ever visit.
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.
Another amusing part is that it's a moving target - some parts of the language actually got better with implementations and spec improvements - so there's a large number of people avoiding useful features on outdated practices (they learned "don't use X" without the reasoning behind it :) )
Templates can be abused, but there's real value to simple ones such as container classes and even shared_ptr and unique_ptr. The subset of C++ I personally use is pretty small, but I hope I never go back to manual memory babysitting.
You suggestions are great if you don't want to use anything like the standard library or just about any other library. Unless you want to write tons of boiler plate wrapping these things up.
This seems to be a failure to understand when exceptions and when error codes are to be used. I'm just starting a big infrastructure project that needs to be high uptime and distributed across a large number of machines, and I'm doing it in C++ because I know I'd never get it working in anything else.
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).
I think you are right about the author not understanding exception handling very well. But your handling of exceptions is also incorrect. In a fault-tolerant system, dns lookup failures or repeated connection attempt failures is something you design for -- they are not exceptional events.
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.
I disagree -- the determinant is about state management and locality of error handling and this is all about how your code is structured, not whether the error is expected or not in some nebulous sense.
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.
Well, the reason exceptions should be reserved for exceptional cases
is because they are gotos. There is no way around that, a function
that both returns values and throws exceptions is more complex than
one that doesn't because you are jumping through call frames.
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.
> separates exceptional situations from in-band normal error handling
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.
For anyone who is curious about the intended use of exceptions in C++ I recommend Jon Kalb's two-part tutorial on exceptions from CppCon 2014. First video is here: https://www.youtube.com/watch?v=W7fIy_54y-w
Yeah, it's a nice example. However, I disagree on the severity of your prognosis.
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.
The slab allocator is a great strategy, but of course only works for memory, which is really a special case of one out of all of the resources we want to manage. If you're writing long running processes you need to care about any number of file descriptors, maybe mutexes, certainly locks.
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.
From my experience, if you need to write many exception handlers then you have a problem in your design as you are using exceptions where you should have used explicit control flow. There is nothing fundamentally wrong with the exceptions and they are used in most modern languages, not only C++.
I completely agree; the entire time I'm reading this I couldn't help but feel this was misunderstanding the use of exceptions.
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.
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 was really struggling to pick up c++, and when I read the FQA I had a 'it's not me, it's you' moment.
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.
Even though I disagree with many of the FQA's recommendations, at least it describes their rationale. Providing the arguments behind the alternative viewpoint allows for an informed choice.
The reason why the charm of C++ wore off on me early on and why C is like my wife who I love more and more each year is that there's a lot of hidden "knowledge" in C++. The language is always playing a game of its own and a lot of your level of expertise depends on how well you've bothered to learn the rules of that game.
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.
Author of the article here. Everything said in the article still applies IMO but, to be fair, C misses some useful modern features, most importantly a decent concurrency support. I've recently tried to remedy the problem by implementing Go-style concurrency in C: http://libmill.org
This might be unrelated, but after reading this 3 years after publication I think that Go approach to error handling i so much better because of exactly the same simplicyty as C.
( If you dont’t understand why, dont’t worry it will be obvious to you in 3 years :-)
>I think that Go approach to error handling i so much better because of exactly the same simplicyty as C.
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.
While it works, it would be better if multiple return types were supported natively by the C++ language precisely because of this:
#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.
It's precisely a C++ issue because 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.
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.
I may have misunderstood your terms, so I apologize if my response doesn't cover what you're talking about.
> 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.
yeah go/rust error handling is great. Much better than exceptions which make the code hard to read since you can't easily predict where is the flow going to jump. And the multiple-return is very convenient in this case too.
Also, with Go's approach you have to either explicitly ignore the error or deal with it.
My problem with Go (and I love the language a lot) is that a lot of my error handling is "stop execution in the function and bubble the error up", which results in my code being littered with if err != nil { return err }, which is a bit annoying.
If I'm doing something wrong here I'd love to know it.
- 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
The only other idea (and it isn't a very good one) I've seen is to put the error as a parameter and let the called function early return if given a non-nil error: http://play.golang.org/p/bRdFWBZp5r
You're not doing anything wrong. And I think it's better that way, to have the error possibilities visible and very understandable. All the context you need to see the error handling is in front of you, not hidden in 25 different files. That's a blessing.
That's exactly why exceptions were invented. They aren't really as magical and hard to understand as the parent poster seems to think. In a theoretical sense, exceptions just do if err != nil { return err } on every call automatically.
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've recently started working in node after spending some time in Go-land and the reverse applies to me. The code I'm writing is a mix of js + Go and I find that in JS I have no sense for what can go wrong since it's not obvious to me if a method will throw an exception or not. However, with Go it forces me to think about my error path.
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.
Exception throwing is the goto of 21st century. The article's examples aren't the worst, it's common seeing exceptions for business rules in Java/.NET land.
Exceptions are just a way for a piece of code to say to its callers: "Sorry guys, this is over my head, I give up, sort this one yourselves." And since most pieces are pretty simple, they encounter these situations routinely. Now, if you want things like nice syntax you positively need exceptions at least as a concept. Otherwise you cannot even add two numbers: take `a + b`, what if the result overflows? What if you write `x = foo(y, bar(z))` and `bar()` fails to deliver? Maybe the try/throw/catch syntax is not that convenient, or maybe the documentation doesn't list all the exceptions that may arise and you feel out of control, or maybe a particular implementation is slow; these are all valid reasons. But I don't see how you can get rid of the idea of exceptions. Well, maybe functional languages have something here, but I'm not an expert in this area; my understanding is that you may define a function as returning either `Error` or `UsefulResult` so you can branch accordingly.
I like Google's guidelines for writing C++. I've worked in a number of groups that independently came up with just about the same subset of C++ (and same set of styles), so those guidelines aren't totally off the wall.
The Google guidelines are... somewhat out of sync... with the intended use of language features as described in the Standard and the recommendations of people associated with it. They seem to take as a starting assumption that the language is misdesigned and major parts of it should simply be avoided or used in unintended ways. If you agree, it's a perfectly fine guide. Otherwise, I'd recommend reading any of Scott Meyers' or Herb Sutter's books and following their advice.
I've read those books. I have a lot of respect for the authors, and they have a lot of good advice.
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++.
As usual, this guy doesn't understand how to use C++. Nobody with any significant C++ experience puts error generating code in constructors. Once I saw that I stopped reading.
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.
Compiling with exceptions turned off is highly unusual. If you do that, you're not really writing C++ anymore, as it's a fundamental part of the language. Exceptions are the mechanism provided for indicating that it was impossible to construct an object, and to trigger appropriate cleanup actions.
> Compiling with exceptions turned off is highly unusual.
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.
I'm a professional, C++ dev and my greatest quibble with C++ is its deranged system of numerical types, implicit conversions between them, and that signed arithmetic can produce UB just about anytime: http://www.boost.org/doc/libs/1_55_0/libs/numeric/conversion...
Well, "defect by design" is still a defect and I have to work around it on almost daily basis. Besides, being able to do (bool+int) could not come from C since C didn't have bool at the time.
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.
Is there any production-level C code which relies on (int+bool, and other semantically meaningless arithmetic) working?
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.
Code that switches over a variable nominally declared to be of type BOOL and handles more than 2 distinct value cases is always such a joy to read; every enterprise grade code base should have at least one of these for entertainment purposes.
I'm curious about that last bit. I occasionally hear this argument that size_t being unsigned is a bad thing. Presumably the standard library designers would claim that a size cannot be negative, so this is appropriate. Why, in your view, should that be a signed quantity?
Unsigned numbers in C++ are completely unsuitable for restricting the domain range because their "rank" (technical term) is higher than that of equally-sized signed types, which means that all operands are promoted to unsigned if one operand is unsigned. Of course, the result is unsigned too.
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.
Interesting article. So, more confirmation C++ is inferior for writing predictable or high performance applications. I actually see an opportunity for a C-compatible or C-competitive systems language here to beat C++ at its own game. Zero-cost abstractions plus solving C++'s specific problems, painless C FFI, and compilation to C (or LLVM) to leverage their compilers. Might make a nice combo. I doubt we'll see much takeup, though, as the technical merits rarely dictate that.
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. :)
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.
I agree strongly; I never really mastered C++, but I've done lots of Java development and enough C developments in anger to consider myself at least a journeyman C developer. (I know enough C++ to be dangerous.)
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++.
My quick perusal of online links show that Objective-C is slower than C++, maybe due to its dynamic typing. It has the C compatibility and basic OOP. Yet, my idea was a language like this which is faster than C++. Maybe a statically typed version of Objective-C would do?
The OO parts of Objective-C will likely be slower than the OO parts of C++ in many cases. There are a few places where ObjC can help even the score on performance. Typically ObjC objects are not copied (cloned). Normal usage (within a collection, ivars, etc.) is with reference counting. Prior to C++11 (shared_ptr, unique_ptr, etc.), there wasn't a good way within the language itself to track object ownership without having to make object copies.
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.
While it's not a C-like language, Nim [0] is an interesting take on how to do a "systems" language, with a very interesting GC story (and a way to turn it off altogether or manually manage memory when required) and amazing C FFI -- I'm not much of a C programmer, but even I've managed to integrate C libraries into Nim. Still not quite what you're after though; I think more something like Go, with manual memory management would be nicer. A "modern C", that takes learnings from C++'s successes and failures, but keeps C's strengths.
I saw it a while back and thought "another toy language that won't deliver." Reading the FAQ, the author quoted as inspiration every alternative language I mentioned in this thread lol. He's done his homework. And the features and compatibility have gotten quite impressive since first release... likely due to him using it to build itself (productivity advantage). And under a MIT license.
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.
>I actually see an opportunity for a C-compatible or C-competitive systems language here to beat C++ at its own game.
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.
Currently, Go makes some decisions that hurt it for high performance systems, bare metal, and so on. Rust barely survived some beginner OS projects without its bloat and performance problems showing up. Once comparison I read showed Rust's compiler caught many bugs that slipped through Go's due to Rust design choices. I'm pulling for Rust in particular because its team made clever, design choices for their safety-vs-performance&control tradeoffs. Yet, right now, both are inferior to C, C++, Ada, the old Modulas, and even Fortran for bare metal applications.
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.
>Currently, Go makes some decisions that hurt it for high performance systems, bare metal, and so on.
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).
Going from what people on different projects and forums said when I was Googling it. I could assess its current state better with a few questions. Can it run in kernel mode on bare metal? Can it run without a sizeable runtime? Are its core abstractions for common uses (arrays, strings, etc) at C++ performance at least? Are basic, demo algorithms small enough as object code to fit into a L1 cache?
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.
Vala is not a C# subset (though it does significantly resemble C#) and really isn't intended or suitable for writing high-performance code or reusable low-level libraries like C or C++ are; it's a way to write GObject-style C without as much boilerplate as it takes in C.
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.
There's no shortage of people taking a stab at language design and even resources to pull it off. The post was intended to provide a simple route for making (a) a better C++ or (b) a ZeroMQ variant with improved robustness & maintenance. Just inspiration and stuff to think about. If I had a budget, a few companies that can get the job done would already have the contract.
Oh, I think I see — you're generously sharing your language-design expertise by guiding us towards working on things whose potential you can see, potential that a less experienced or talented person might miss?
Close, but that's an elitist attitude. More like I help things along by sharing lessons learned from thousands of academic papers, industrial case studies, and so on that I've read. Our industry has a problem where it continuously forgets past accomplishments and solutions. An example is the "can an OS be written in a managed language?" question that keeps popping up despite it being done a half-dozen times. Or endless tactics to avoid stack-smashing while MULTICS's reverse stack solved the root problem in early 70's. Can we scale linearly to thousands of nodes with strong consistency? NonStop and some MPP's did that in the 80's while publishing how.
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.
I used to think the same way. In fact, I would reference this article to reinforce my beliefs. But then I actually took the time to relearn C++14 the right way using Stroustrup's Programming Principles book. A project that was horrific in C became ridiculously easy in C++. And I didn't use a single malloc.
If you read part II (the link to it is near the end of the post), you see that one of the things he wanted was intrusive linked lists.
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.)
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.
How is it possible to write a no-allocation intrusive linked list without move constructors ? When you move one of the nodes (or sentinel if you use one) it would invalidate the links in other nodes.
The list takes an OwningPointer to the object to be inserted. One of the requirements of this trait (which is unsafe to implement) is "the object cannot be moved while in the `LinkedList`." So Box would work, or a mutable reference would as well (the library implements the trait for these types already). The list itself doesn't do the allocation though.
> 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.
I think that the requirement is that anyone who releases a program that was linked with my library must also release it in a form that allows it to be relinked without the library. I'm not sure how that applies to Rust's linkage model.
He could write his own link list template that only worked with LinkedListNode objects; anything you want to put in the list must inherit from LinkedListNode, which would keep the next/prev pointers. Seems like that would be the best of both worlds.
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...