I tend to disagree. (Modern) C++ is an incredibly powerful programming language. Contrary to some other languages it gives the developer maximal freedom and does not impose a particular way of doing things on the developer.
> Contrary to some other languages it gives the developer maximal freedom and does not impose a particular way of doing things on the developer
You seem to be implying that's a good thing. It's literally not.
Imagine if I created a programming language where every random string of characters was a valid program (cue the Perl jokes). Clearly this language permits even more freedom than C++, but this would be a nightmare for programming.
Constrained structure is essential to programming. Sometimes you need to beyond the constraints of a more common language, in which case C++ might be a good choice, but that's the exception not the rule.
Your assumption seems to be that if:
1) an existing language has finite utility
2) a language where any string is a valid program has no utility
, that it must be true that utility decreases with the unconstrained-ness of a language (and thus increases with more constraint).
However, this is not true. You only have to look to the other extreme to see there must be a middle ground. A language where there is only one valid program has no more utility than one where any string is a valid program.
Because this relationship of constraint and utility is clearly not simple, we can't use those extrema to judge if C++ is less useful because it gives so much control. There might be some "local extrema" where a language fits a niche. C++ might fill that niche, or it might not, but I think it needs a bit more of a nuanced consideration than "less constraint, bad".
> Your assumption seems to be that if: 1) an existing language has finite utility 2) a language where any string is a valid program has no utility, that it must be true that utility decreases with the unconstrained-ness of a language (and thus increases with more constraint).
The converse is actually the OP's argument, ie. that a language's utility increases with unconstrainedness. I merely showed that to be false, and argued that constraints are essential, but nowhere did I suggest that utility scales with the number of constraints.
In theory, yes. In practice few people use C++ fully, too often you find in-house "style-guides" vetoing specific things, such as Google's famous "no exceptions".
At the point you rule out using available facilities of the language you might as well use something else.
If I recall correctly, Google's rationale regarding exceptions is that their legacy code is not exception-safe, and so they were faced with the choice of either rewriting critical parts of their legacy code to handle exceptions, or don't use them.
Also, their "no exceptions" rule only applied to work involving their legacy code.
I'm too lazy to find the source, but that bit of trivia was already discussed ad nauseum even in HN.
The morale of the story is that you should not mindlessly repeat any opinion without knowing the rationale and instead pulling appeals to authority to cover up the logical hole. That's how you end up contradicting even your source, just because you believed that's how the cool kids do things.
absl::Status is basically exceptions without language sugar. Or, another way of looking at it, golang err in C++. Basically, non-local control flow is dangerous as it can't be evident to the programmer when something deep in the stack will bubble up an exception.
It is super annoying to deal with sometimes but there are a lot of amazing helper macros (yuck for other reasons) that exist. More details can be found looking through here: https://cs.opensource.google/search?q=ASSIGN_OR_RETURN&sq=
> Basically, non-local control flow is dangerous (...)
This is the crux of the error you're making. Exceptions are not about control flow. Exceptions are a transparent and clean way to handle exceptional events. Exceptions are not intended to, say, handle the status code of a HTTP requests. Exceptions are intended to handle exceptional and potentially unrecoverable errors in a safe and controlled manner, such as failing to allocate memory, regardless of where and how they pop up.
Therefore, suggesting classical C-style return codes or specialized monadic types to handle results as alternatives to exceptions completely misses the whole point of exceptions and, more importantly, all the classes of problems they are designed to eliminate.
> Exceptions are a transparent and clean way to handle exceptional events.
I'd sum up this argument as: Exceptions are syntax sugar that allow callers of a function to ignore the error cases of something they are calling and depend on something that calls into them to handle the error case.
In this example KV.set will raise an exception. The programmer implementing HandleRequest isn't directly exposed to this fact and so to them, and the reviewer, and future onlookers this code looks correct. Now lets say kv.set() throws an exception in production under a specific case. Maybe there were two people attempting to set the same value at the same time or a networking issue. Doing this in the context of a webserver might make sense as the webserver might handle exceptions as error codes but that's not the end-all-be-all. Suppose we refactored to something like this
Something CreateSomething(....);
How do you know if this function will throw an exception? How do you find all of the possible exceptions that can be thrown? Statically you can't really. If instead you see
absl::StatusOr<Something> CreateSomething(...);
You can tell for sure that the result has some error that needs to be handled. Your original code:
kv.set("a", 10);
This code no longer compiles in an absl::Status world. Instead you'd need to do something like:
CHECK_OK(kv.set("a", 10)) // this will panic
RETURN_IF_ERROR(kv.set("a", 10)) // Bubble up
From this we get:
1. a stack trace since RETURN_IF_ERROR adds metadata about the call site.
2. Guarantee that code will not compile if errors are not handled.
3. Guarantee that future readers know that some very high level function could probably call into code that can produce an error you need to handle.
This matters much more if things like this are happening:
There are cases where destructors do not make sense. You do not always want to call `doNextStep()` as it would be 100% wrong in the case where we cannot set our value in our kv store. Contrived but I've run into these in real life services. If a developer sends me the above code snippet I might LGTM. If the developer instead sends me:
otherService.startingWork();
if (!kv.set(...).ok()) { log("something went wrong"); }
otherService.doNextStep();
I'll be able to point to the exact problem with this code much more easily. Also if there's an outage and I need to read this code I can clearly see why this `something went wrong` in the logs correlates to incorrectly called doNextStep().
I'm not saying that Status is perfect (I'm not 100% sold) but exceptions are a type of control flow in an abstract sense. The problem some people have with it is it's control flow you can't audit.
Exceptions are a way to ensure that failures are directed immediately to a place designated to deal with such a failure.
They are, in particular, not any sort of "syntax sugar", unlike "?" in certain other languages, or your StatusOr thing. A function that throws an exception does not, in any sense, return to its caller. It does not construct any sort of return value. It does not consult the stack to see where it came from and resume running there.
And, exceptions are totally auditable. There is never any hint of ambiguity or uncertainty about where an exception will take you.
You can be confident that if a function was thrown from, it was because it could not perform the requested action. And, you can be confident that if an exception was not thrown the function called satisfied whatever postconditions it promised.
So, you don't need to know if a function might throw. You may instead assume any function might throw. If it doesn't, then it has satisfied its documented postconditions. Your obligation is only to ensure that destructors clean up any intermediate state on the way out. These identical destructors get exercised every time through the code, so are exercised frequently.
Error-handling code at places where the error cannot actually be dealt with properly, that just tries to propagate the failure up the call chain, is typically not well tested, and often cannot even be triggered in testing.
Indeed the details matter, but my example was just designed to be something that might be somewhat more recognizable than those situations and restrictions I've encountered personally.
> (...) my example was just designed to be something that might be somewhat more recognizable (...)
If your goal was to use that poor example to support the idea that people stick with subsets of C++ because of reasons, that example failed to support the assertion. Thus it makes no sense to stick with a patently wrong observation just because it's easier to recall.
Getting back to the topic, as far as I know there are only two features of C++ which are up for debate regarding their adoption: exceptions, and template metaprogramming. The exception-handling debate only makes sense in very low-level applications and refactoring legacy exception-less code, which in practice is not anyone's case. The template metaprogramming debate typically boils down to YAGNI and the need to avoid resume-driven development. Nevertheless, both features are used extensively, whether directly or indirectly (see STL), and in general there is no reason to bother debating whether people should use it or not, unless you have very specific requirements in mind (I.e., avoid generating magical code in embedded applications, or in very high performance applications where you feel you need tight control over everything down to which instructions are generated).
Practical, old languages tend to do this. In C++ or Common Lisp, which share little other than being multi-paradigm (unopinionated), it is fairly common to have house styles or accepted subsets.
Languages that try to build in some "house style" are my personal dystopia - such as early Java or current Go. Which does not say they ate not effective, just that I personally hate the philosophy.
Both C++ and Common Lisp suffer a huge accumulation of historical baggage. It is this that makes the languages problematic, not being multi-paradigm. Common Lisp has first/rest as well as car/cdr. It has streams and numbers and the functions on them seem generic but aren’t (always) generic functions. It still has rplca despite (setf car) being a valid function name.
Sure; but "historical baggage" is a function of multi-paradigm and outliving multiple generations of computer architecture. I would not view baggage as necessarily problematic. What makes C++ problematic is that template metaprogramming evolved in a very ad hoc way, and now we need to backwards compatible all of it.
My claim is that these multi-paradigm languages may be better thought of as a core plus some paradigm-specific sublanguages, all jammed into the same syntax and glued together in random places. A better multi-paradigm language could avoid having separate parts that poorly work together. The thing that the comment I first replied to called ‘multi-paradigm’ is I think really the bad glue job like a plate that was smashed, glued together, smashed, and then glued together a second time. I don’t think it is an essential quality of multi-paradigm languages. For example Raku (fka perl6) fits different paradigms together more smoothly, and Julia can be used in a procedural or functional way as well as a more object-oriented way (though Julia’s objects tend not to contain as much state as a typical object-oriented language)
> In theory, yes. In practice few people use C++ fully, too often you find in-house "style-guides" vetoing specific things, such as Google's famous "no exceptions".
Almost never seen that in practice. I always see people talking about it in forums, but in real-world gigs I don't know people who artificially restrict their codebase with braindead rules like that.
You haven't seen safety relevant code then. High SIL and ASIL levels (combined with those systems being embedded) result in such rule sets for a good reason.
I have yet to see more than "print and bail out" in catch blocks. In embedded there is nobody who can read your cry for help and especially in fail-op systems this is just not an option.
Herbceptions are just not there yet and until then we help ourselves with things like "expected" for example.
Enjoy the source code of Android, Windows and macOS frameworks.
In fact probably yet another reason why Apple and Google aren't in an hurry to improve clang to latest ISO, and other companies in clang ecosystem even less.
You seem to be implying that's a good thing. It's literally not.
Imagine if I created a programming language where every random string of characters was a valid program (cue the Perl jokes). Clearly this language permits even more freedom than C++, but this would be a nightmare for programming.
Constrained structure is essential to programming. Sometimes you need to beyond the constraints of a more common language, in which case C++ might be a good choice, but that's the exception not the rule.