C++ Papercuts
thecodedmessage.com
thecodedmessage.com
My feeling during the read, and my definite conclusion afterwards, is that I didn't read a collection of new nice additions to the language, but a list of minefields and new fancy ways to crash your program. That there is no chapter which doesn't end up being a list of gotchas. Seemingly every new feature was added as an extension of the language's reputation of being a shotgun that loves your feet. "Oh, now we have lambdas! here are the 32 ways you can fail while using them, and the language will do nothing to help you prevent".
Effective Modern C++ is a very well written book and very instructive. Its flaws are none of the book or writer, but of the language itself.
I also agree that the new features seem to embrace and extend the "footgun-ness" of C++, rather than attempt to fix it.
That's much easier to do when the tasks that the language is suitable for are bounded in some way.
There are not many common idioms that are going to be shared (beyong libstdc++) between a realtime audio processing application and a data visualization backend for the web and an embedded system control app and a payroll processing backend.
An example of a language with a smaller domain which makes its idioms obvious through its design is Ruby.
An example of a high-performance language that can be used for all those things that makes its idioms obvious through its design is Rust. Another would be, imo, C.
> There are not many common idioms that are going to be shared (beyong libstdc++) between a realtime audio processing application and a data visualization backend for the web and an embedded system control app and a payroll processing backend.
I also just generally have to disagree with it, but you seem to be using a different definition of idiom to me. From what you said here, you seem to take idiom to be synonymous with "design decision". For me, an idiom is a specific semantic construct, possibly associated with specific syntax, that achieves a specific basic programming task. For example, the idiomatic way to loop with an index in Python is to use enumerate.
It's merely the way that most people do the basic things, in a way that is actually good to do. C++ is infamous for a huge number of people using the language dangerously, to the point where there are effectively no idioms outside of specialist communities, and to find them out you need to read books. A significant amount of the basic design of the language needs to be ignored for most purposes.
While I wouldn't say no to having such idioms available in C++ as language constructs, it is important that C++ continues to provide the lowest level that it can for such tasks (e.g. pointers into arrays or other buffers), and probably the level right above that (libstdc++).
Similarly for threads (or more generally, parallel and asynchronous programming) - some other languages make available some really nice idioms for expressing these things, but again it is important for performance and control reasons that C++ continues to provide the current lowest level of access for this (basically a wrapper around pthreads). While things like promises, futures and coprocessing are really useful, they are often inappropriate for highly performant code.
I don't have the impression that Rust's container idioms match those of, say, Python, but I may just not know Rust well enough.
E.g. https://github.com/glium/glium
Rust would be useless if you couldn’t have safety if you had any unsafe at any point. That’s the whole point of rust: you can build safe interfaces to unsafe actions
One of the sources of complexity in the C++ I work with is that a lot of it is using templates to try to maximize the size of individual expressions. Consider, for example, the Eigen library (https://eigen.tuxfamily.org/index.php), which gains most of its magic in using compile-time template specialization to re-group logical operations so that the compiler can realize "Oh, this is really looping over dozens of memory cells to do the same operation... I should compile it using SSE instructions." I haven't seen yet whether / how Rust pulls off similar tricks or whether doing so in Rust requires a developer to write n-ary asm bindings, which would actually make Rust less expressive than C++ if the goal is maximizing performance with minimum code.
(Of course, the flip side of that is that there's a lot of Eigen that's unsafe behavior; hold the library slightly wrong, and you're operating on deallocated memory. Oops. ;) ).
That being said I feel Rust has a lot of paper cuts that slow people down too. Perhaps the silver lining is that they are more like barricades than minefields.
As a Rust newby I think they are more like achievements that unlock new capabilities.
"and, in fact, it is a "contested illness" due to doubts about the legitimacy of the condition."
AND
"It was noted that in this case, however, the police were perceived to have acted with little care for the hostages' safety,[6] providing an alternative reason for their unwillingness to testify."
Somehow appropriate considering this thread is about how someone's bad experience with C++ has driven them into learning rust.
Beginners, count your blessings. You don't need to unlearn other languages before learning Rust.
I sincerely believe the only keywords common to Rust and C are a few control flow keywords (for, if, while; but not switch, goto, or do-while) and "bool"---the latter only because of C99 and stdbool.h
I think readability is a major part why java was hugely successful: C programmers could read java code and immediately understand what the code did and able to make minor modifications to the code.
This is very difficult with Rust code. Rust "learned" too much from Perl in this regard.
Here's Barry Revzin, a C++ programmer, explaining what he wants with a Rust example
https://brevzin.github.io/c++/2020/12/01/tag-invoke/#this-is...
Now like I said, I'm a Rust programmer, so of course I understand PartialEq's definition there, but, is it really hard to see what's going on as a C programmer? There's a lot of magic you don't have, you don't have traits, or references, or the Self type or the self keyword, or really any of the mechanics here, and yet it seems pretty obvious what's going on, doesn't it?
I literally don't remember any unpleasant surprises of this form when learning Rust. Actually mostly the opposite, especially for != I thought, from experience with other languages like Java, C++ or Python, OK obviously at some point the other shoe drops and they show me that I need a deep comparison feature. Nope, Rust's comparison operators are for comparing things, it isn't used to ask about some surface programmer implementation detail like the address of an object in memory unless you specifically ask for that. If Goose implements PartialEq, so that we can ask if one Goose is the same as another Goose, then HashSet<Goose> also implements PartialEq, so we can ask if this set of geese is the same as another, and HashMap<Goose,String> likewise, so we can ask if these two maps from geese to strings are mapping the same geese to the same strings.
I know ripgrep is written in Rust and works really well and "does one thing", so I'll go to its repo ( https://github.com/BurntSushi/ripgrep ) and read through a bit. First, I'm looking for a "src" folder, but that does not exist, so right off the bat I am a little uncomfortable. Nothing in the root folder looks like it would be the source. What are "complete" and "scripts" and "pkg" folders? Because those (in that order) would be what I check.
"complete" looks like command line completion. The "scripts" directory has one file, "copy-examples". "pkg" contains folders "brew" and "windows". Still lost...
I thought "crates" was like modules for Python, so I think that would only contain dependencies and stay out of there.
Finally, I relent and open "build.rs" which I suspect is something like "build.ninja" and I can figure out which node has source files. Stupid me. "build.rs" IS the source file.
Oh. No, actually, it's not all of ripgrep, in fact. It's the tip of the iceberg. The source is in crates/core/app.rs, and build does manpage compilation, registers shell completions, etc.
So, line one. I don't know what the return value App<'static, 'static> is. I understand it's an App object, and the tick (no idea what it's called; this is starting to feel like a fracture mechanics lecture) I know has to do with ownership. It's tough to see how there are two subtypes in App<> when the assignment of app has ten functions that assign attributes. I can certainly READ what is being assigned to the App object. I wouldn't be able to WRITE any of this so far. Unimportant, it's not the meat and potatoes of ripgrep.
Now I'm scrolling, looking for something cool to analyze and run across Vec<&'static str>. Okay, I thought tick was ownership, but I also thought ampersand was ownership-related. Maybe one is mutability? (This is like knowing how to play music really well but now learning a totally foreign instrument.)
Skimming, it looks like this whole file is the argc/argv parser. I'll look for something interesting in main.rs, search.rs, and subject.rs (the last because it's an unusual name).
struct Config {
strip_dot_prefix: bool,
}
Well. I can read that! Next line... impl Default for Config {
...
...not that. It's probably something like a class method.As I'm reading through, it feels like a LOT of kicking the can down the road and verbosity. There's a function binary_detection_explicit(self, detection) that just assigns detection to self.config.binary_explicit and returns self. binary_detection_implicit() does basically the same with a different assignment member. getters and setters, roadblocking readability since the dawn of computer science... (a few lines later, the function has_match() returns the value of has_match. ugh. This is why I prefer crudely written C to textbook C++.)
I see .unwrap() in a line. This tripped me up during the tutorial; still no idea what it does. Sure I can look it up, but there are a hundred other terms to learn: unwind, map, Result (OH! The two App return types are probably Ok and Err ... maybe?), convert, concat, const, continue, ...
subject.rs, btw, can probably be written in three lines of really dense Python or ten lines of really well-written Python with a few other lines of Doxygen. Instead, it's about 90 lines of Rust with another 50 lines of comments.
Finally, I find something noteworthy.
fn search(...) ...
fn iter(...) ...
for subject in subjects {
searched = true;
let search_result = match searcher.search(&subject) {
....
This is all extremely readable and just feels like a standard queue in any language. But then there are lines like: if let Some(ref mut stats) = stats {
*stats += search_result.stats().unwrap();
}
I give up. Are you assigning the variable stats to itself? by reference but mutable? If Rust doesn't have pointers, what is *stats? unwrap() is no longer the most confusing part of this.-----
Postface, I'm originally a mechanical engineer who eventually got roped into writing driver software, so C is my comfort zone and I don't need anything much more complicated than "set bits here, read registers, use a 40-year-old communication protocol, handle errors".
There's simply no easy analog between Rust and Python ... or Rust and C ... or Rust and SQL ... or Rust and PRACTICE ... or Rust and any other language I've learned. I understand a lot of the CWE Top 25 software errors are mitigated by Rust and it's not just a theoretically correct but unusable language, but there hasn't been a strong reason to learn it, and the learning curve is made steeper by preconceived notions of every keyword.
-----
I forgot to compare with grep.c
Extremely readable. I know right away which part is args, where memory allocation happens, where file handling occurs, and then the "hard part"---where the magic happens---is the last page on my monitor. I can just stare at that and mentally deconstruct until pieces fall into place... Look up regcomp()? check. Look up grep_tree()? check. Read the kernel.org discussion on why grep_tree() was written and how it relates to ensure_full_index(), which explains some of the other variables.
The whole thing sits nicely in a few pages with terse but clear comments (unlike this post) and looks familiar even though I've never seen the source before.
-----
Last edit. I tried just now to refresh on .unwrap(). https://doc.rust-lang.org/rust-by-example/error/option_unwra...
Only because of the comments did I realize that calling drink.unwrap() will panic (throw an error?) if that argument is empty. So, unwrap is just the "if (... == nullptr) { return ...; }" of Rust, it seems.
But it's somehow an alternative to .expect() and has its own alternatives, unwrap_or/unwrap_or_else/unwrap_or_default, and it also seems optional. And my time in the rabbit hole is over; my children need to go to bed.
This comment reminds me very strongly of myself, trying to read German. You know C, why shouldn't you be able to understand Rust? But they're not the same thing, and they communicate different things in different ways. There are concepts that simply do not map directly. Someone could very easily write the exact same post from the perspective of a Rust programmer starting with C. Where is the equivalent to Cargo.toml? What is this Makefile thing, and why doesn't it look like a configuration file? Etc. No amount of just forcing yourself to try is going to provide clarity unless you already understand the idioms and structure at play.
With that said, I have written embedded Rust and it is an amazingly freeing experience. Writing C right now feels like trying to balance a knife on my finger. With Rust I can just get work done. I strongly recommend you spend the time to learn it. Even if it doesn't convince you that C's days are numbered, it will make you a better programmer by training you to think about safety and correctness up front.
-----
I tried Embedded Rust about 4 to 6 years ago and found the experience underwhelming, as I was installing a TON of packages just to get Blinky and seeming to bypass a lot of standard Rust (no_std, no_main, use panic_halt as _, everything GPIO is unsafe, etc). The last straw was that imported Cargo packages had their absolute path somehow hardcoded in the symbol table, so my debugger would try to load, like... C:\Users\jacob\stm32-rust-thingy\app\solve.rs (and fail, because I'm not jacob and don't have his source code)
I'll give it another shot. I'm sure the "many eyes" principle (and better/more tutorials in the past few years) make the process easier now than then.
And German has a particularly special trap for "trying to parse out each word": trennbare Verben (separable verbs). Unless you know enough of the grammar to know when a piece of the verb has been unceremoniously punted to the end of the sentence, trying to use a dictionary to find the meaning of the verb of a sentence can be an exercise in futility.
I'm reminded of a Mexican friends mother, she and her aunts went to Europe. They loved loved Italy. Partly because if an Italian spoke slow and clearly they could understand what the person was saying.
My experience with C# for instance was it was really easy to pickup. Because snippets of code are generally C like and procedural.
Heard the above about said about go. If you know Java and JavaScript you probably know go already.
I found python not too hard because I knew perl sort of. Mostly enough experience with it to avoid it.
I suspect if you know C++ and a ML language Rust is a breath of fresh air. But if you don't it's not. It's gross. And very very slow iteration times.
As long as I stick to Embedded Rust, I can use work time to learn more. I'll probably plan that between my current projects these next few months.
You look at the code and go what? Then you cock your head and it's still what. And then you turn your head sideways and suddenly it makes sense. Then you look at code in another language and go what?
I'm not a database expert, but I became comfortable with SQL this past year so I could better understand how my company's office software is structured. I can cobble together enough Javascript to create an own online sheet music book using abcjs, a directory full of ABC files, and jquery. I was able to barge through enough C# to write a crude 2D game during a game hackathon in college.
In fact, AFTER learning Python, a lot of C became easier because I knew HOW a data structure should behave, which then influenced how I would create it from structs and functions.
However.
I get hung up on Kotlin. Kotlin reminds me a lot of Rust in its syntax, and Android has the same habit of renaming every single concept.
In general, the more keywords and syntaxes two languages have in common, the easier to read. (In SQL's case, I think the relative lack of keywords and rigid query structure made this a nonissue.)
So, mixed bag.
IMO ripgrep is a poor example of project layout both because it does "non-standard" things and because it encompasses more than the C-based grep you're comparing it to. A simple project will almost always have its source in a directory named "src", and a project with multiple subprojects (like ripgrep) will typically not shove them into a "crates" directory.
Compared to GNU grep, the ripgrep project serves as the repository for a bunch of libraries as well as the ripgrep binary. So there's already a lot more moving parts than the name "ripgrep" might otherwise suggest.
I don't know Java, but I can read the Falstad circuit simulator source.
If you can read Java, I'm surprised impl blocks threw you for a loop as the first comparison that comes to mind is that with Java interfaces. A quick look at the Falstead simulator suggests it's using a pretty small subset of the Java language (e.g. no generics, interfaces, or exceptions) which would certainly make it seem a lot like C. Not all Java is like that.& and *? Akin to reference and dereference in C, and given that you can overload both the comparison to C++ seems pretty apt.
In any case, for more direct explanations the API reference is a better place to look. Two short sentences explain what unwrap and explain do, and what the differences are.
This is not unique to ripgrep; for example, that's matklad's general recommendation https://matklad.github.io/2021/08/22/large-rust-workspaces.h...
I have split out some crates that obviously stand-alone. The `termcolor` crate was born inside of ripgrep but now it's in its own repository. The `ignore` is another candidate for splitting out because it has broad utility, but it needs a lot of work before it's something that can standalone as its own project IMO. But most of the rest of the crates, i.e., grep-{matcher,searcher,printer,regex,pcre2} are essentially what ripgrep is. Those will never get split out. If I did, the ripgrep repo wouldn't actually contain the most interesting parts of ripgrep! Not good.
With all that said, I am a monorepo guy. Not for ideological reasons. For practical reasons. I also maintain dozens of crates, most of which are in their own repositories. The only reason I do a different repository for each because that's what the custom is and it makes collaborating with others easier. If the code was just for me, it would all be in one big mono-repo. No question. It would be A LOT less work.
With all that said, I am a monorepo guy. Not for ideological reasons.
For practical reasons.
Yep. It's a balance. The big problem I have with large(r) repos is that everything git related is just slower and takes way more space. I've got some git prompt magic (using gitoxide of course) and it just drags with the rust repo.At one point I wanted to make some changes to a man page in FreeBSD. Sure, it's neat that there's thirty years of history. But it's thirty years of everything's history. Shallow cloning made it a bit more bearable, but the UX for trying to grab just a directory or two from a repo is still petty gnarly.
But it's all just emacs vs vi all over again (and I've settled on helix and sublime anyhow).
You are absolutely right. The foreign-ness you are seeing is the ML side of Rust’s roots. Rebinding, restructuring, small wrapper types, and of course all the functional programming bits and bobs that people coming from, say, Haskell might see as being quaintly simple, but arcane to anyone else.
If I see Java source, it makes sense even though I don't know Java... at least up until how javax.swing layouts are constructed or I see a really obscure annotation without comments. If I had to add something to the Arduino 1.0 IDE, I'm confident I could figure it out in a day or two, because a class is a class, lines end with semicolons and comments look /* like this */, I can guess the existence of sort() or random() or an int being either int, Int, or Integer.
In contrast, a Rust "trait" being roughly equivalent to a C "struct" would not have been in my top 20 guesses. I can't recall trait being a keyword in ANY language I've used. I have no idea what Box and Arc are, because to me, those are "what you put tools in" and "plasma from too much potential difference".
Yes, I can check the reference (constantly) but at some point looking up every word or tick mark gets in the way of absorbing the story. The punctuation in particular bothers me, because it's not searchable via engine. There's no special Google result for
rust '
I can't be more clear. My ability to read/write code extends beyond pseudocode. Rust is just not an easy transition for a "primarily C" user, in my experience and opinion.“Trait” is used for similar features in Scala and PHP. https://en.wikipedia.org/wiki/Trait_(computer_programming)
Box is used in Java and many functional languages. https://en.wikipedia.org/wiki/Boxing_(computer_science)
Arc is the most confusing name out of these for sure; most folks talk about “reference counting” in general and assume the count is atomic; with Rust’s performance and safety focus, Rc is the non-reference counted version and Arc is the atomically reference counted version. Additionally, there is a feature called “automatic reference counting” that some languages use, that uses atomic reference counting in the implementation.
> There's no special Google result for rust '
If I google for “rust apostrophe” this is my first result, which is pretty comprehensive: https://stackoverflow.com/questions/22048673/what-are-the-id...
While this syntax is unusual it is also not unique, OCaml uses it for similar purposes. Nobody loves this one, but also nobody was able to come up with something that could be agreed upon to be better.
Typo here I think Steve, Rc is reference counted, but it's not atomically reference counted. Hence the lack of an A. You've managed to instead say that it's not reference counted.
ripgrep is way more abstracted than most greps because ripgrep splits a whole bunch of its functionality into crates. The idea being that others can then re-use the reasonably sophisticated infrastructure that ripgrep uses to write their own bespoke grep tools. ripgrep internals are more like a bare-bones and under-developed grep framework. If you go back to the initial version of ripgrep (0.2.0), you'll probably find it smaller and significantly easier to read. There's a lot less abstraction. FWIW, I generally regard my abstraction attempts here to be a partial failure. The hit to code readability has been quite large. I also fucked up at least one abstraction boundary. On the other hand, it has enabled people to maintain very small patches to make ripgrep work with other regex engines.[1] (And of course, ripgrep uses the abstraction to make it work with both Rust's regex crate and PCRE2.)
I wouldn't call Rust an easy language to learn. It could be easier depending on your background. For example, if you have an ML/Haskell and C++ background, then Rust will probably introduce very few things you haven't seen before (that being the borrow checker). But if your background is, say, Python, Java and C, then there are going to be oodles of things in Rust that will be novel to you. That basic vocabulary will be more difficult to acquire.
I'm somewhat surprised you feel confident enough in your understanding of Rust to declare you could replace 50 lines with 2 or 3 lines in Python though. Meh.
> getters and setters, roadblocking readability since the dawn of computer science...
They are tools of encapsulation. I don't treat them as boiler-plate. I treat them as things that I use when I care about encapsulation. And when I don't care about encapsulation, I don't use them. My personal style is to lean heavily on encapsulation.
also, true about rewriting subject.rs, that was a bit of hyperbole about how everything spreads out vertically and seems to do one task per screen-full of text. I'd need at least as many lines as functions and classes and returns, so about 40 keeping everything clean and not combining/deleting classes. which is what you have if you combined your multiline statements.
(aside, i don't need to know how to read rust to understand subject.rs. The comments describe everything in plaintext)
As for the source code itself, I agree, Rust does rely more on library functions than language constructs on some things (e.g. unwrapping optional values), but I'd argue this is just more different form C rather than strictly worse when it comes to readability. I wouldn't complain about how difficult it is to read Greek as a person who only knows English.
I can't say I find Rust's pattern matching rules particularly pleasant. The rules are convoluted and arbitrary
https://doc.rust-lang.org/stable/reference/patterns.html
Also, it has often been surprising to me how code that looks like it's entirely read-only will try to move stuff.
E.g, this works:
let numbers = [1, 2, 3];
for n in numbers {
println!("{}", n);
}
for n in numbers {
println!("{}", n);
}
This doesn't work: let numbers2 = vec![1, 2, 3];
for n in numbers2 {
println!("{}", n);
}
for n in numbers2 {
println!("{}", n);
}
I understand why that is and I know how to fix it, but I don't like it. There is a long list of things in Rust that I find understandable but also unpleasant. In that sense it really is a worthy successor to C++. Smart people making unpleasant decisions for good reasons.The reason the first one works is that [T; N] is Copy if T is Copy, and i32 is Copy so therefore [i32; 3] is Copy. So it's actually consuming two arrays, each of those for loops consumes the array to make an iterator, but since it's Copy it just leaves a perfectly good copy of the array behind in the variable. The compiler can see what we're doing here and won't pointlessly duplicate the array (presumably) but that's conceptually what's happening.
The second one doesn't work because the Vec<i32> isn't Copy, so, we consume it and now it's gone, when we reach the second for loop the variable numbers2 is gone already.
So you shouldn't make assumptions whether something will be moved or not based on read only preconditions.
let s1 = String::from("abc");
{
let s2 = s1;
}
println!("{}", s1); //borrow of moved value: `s1`
But I do understand what you're getting at. In a sense, a move is not a runtime concept at all. The compiler simply considers the variable as no longer readable unless and until a new value is actually written to it.Uh... maybe? But Rust is more like a replacement to C/C++. So a better question is that whether C#/Java programmers could read C/C++ code and immediately understand those & and *.
It is not at all as easy for C programmers to become Rust programmers as you need to learn a lot about Rust before you are able to understand the Rust code.
Just like Rust, C# has an unsafe superset of the language. The meaning of all these * in unsafe C# exactly matches C.
This isn't some abstract "readability", though, this is "how close are your langauges in the evolutionary tree". Where ALGOL is the proto-indo-european of quite a lot of langauges, but not all.
https://doc.rust-lang.org/reference/keywords.html https://en.cppreference.com/w/cpp/keyword
And they're even both provided in (mostly) alphabetical order, which makes this easier for humans
Rust doesn't reserve type names as keywords, which seems like an eccentric choice in C++ (e.g. wchar_t is a keyword!) although perhaps given how fraught parsing is in C++ they had no choice.
The common keywords are: break, const, continue, else, enum, extern, false, for, if, return, static, struct, true, union and while.
true and false are just boolean values, although given C++ like many languages considers "false" to be true, and 0 to be false, I guess it's a surprise they bothered making these keywords.
const, enum, union, static, and struct are about types and values. The choice to have "const" mean "immutable" not "constant" in C++ is hilarious, yeah that's going to cause wider issues in how your programmers understand software. I think C++ enum is rather sad, here's a whole kind of type and it's basically abandoned, it's not allowed to have features the other types have, for no discernible reason. Rust's enum really shows how much opportunity was wasted there.
break, continue, else, for, if, return and while are indeed control flow keywords, although what they do in Rust varies quite a lot from C++. Rust's for is a for-each, it consumes an Iterator (literally, it's a syntax sugar for a loop which uses IntoInterator::into_iter and breaks when the iterator is exhausted) and Rust's break can break-label-value, which is a violently different thing than the C++ break even though the obvious beginner syntax looks identical.
try, do and virtual are reserved in Rust but are not currently used (in stable Rust) to mean anything, we can expect that try is likely to some day be used for a related but different purpose to its cousin in C++.
Immutable is immutable for some specific span of time. In a simple case, like with Python immutable objects, it is constructed once, and after that, it never changes. With Rust, it's also possible for an object to be immutable while a reference to it exists and is passed to some function, but after that function call returns and the reference ceases existing, immutability ends.
fn take_foo(x: Foo) -> Foo {
let mut x = x;
x.field = 0;
x
}
The ownership semantics ensure that because there's a single owner of Foo at all times, it is always safe to make its binding mutable. This is different than trying to turn a &Foo into a &mut Foo, that's not allowed by safe Rust (and an undefined behavior minefield in unsafe Rust).For constants, you can think of them as a "stamp", whatever the value within the const gets inlined in the usage place in your code, every time. Immutability is about whether the value can be changed. To stretch the analogy, you can stamp a pattern on paper (const) but later doodle over it (mutability).
You can see that not only can't you change these values, it doesn't even mean anything to speak about doing so, they're not variables which have values, they're the values themselves.
Whereas an example of an immutable variable would be the std::vector we're being asked to check the length of in a call to size() - this certainly is a variable, somebody else can change it, but we must not.
If we name a constant in Rust like { const TIMEOUT: u32 = 1234; }, we're not saying we want a variable with this name TIMEOUT and then we mustn't change it - instead we are giving a name to this arbitrary constant TIMEOUT (and it has to be constant, Rust won't let us make constants unless they actually are) and then any time we say that name, we get the same value, not the same variable, there is no variable, the same value, the constant one, as if we'd written it here.
So for example the C++ trick where you "cast away" the const-ness doesn't make any sense in Rust because those aren't immutable variables, we can't even try to make them mutable, they're not variables. It's not Undefined Behaviour, it's gibberish, even C++ won't let you write false = true; for example, that's just clearly nonsense.
Not quite, those are literals values.
As mentioned in the C++ standards, const-ness is a quality of variables... Not of values.
In C, wchar_t is a typedef, which in C++ would cause the problem that overloads could non-portably conflict with one of the builtin integer types. That’s why wchar_t was made a fundamental type, and thus a keyword like all the other integer types.
In C/C++, any value can be cast to void, which should be sensible as any pointer can decay to a pointer to void. Thereby, the Wikipedia definition of the void type even says it is like a unit type, and the page for the unit type notes 1) the void keyword simulates a unit type and 2) that--and I think this is very important, to get into the mindset of this term--that the actual unit object type in Java is named Void.
It does really really suck that you can't declare a variable of type void and that some compilers decided the size was 1 for long enough that the size is now undefined (instead of 0), as these mistakes lead to a ton of ridiculous code duplication in C++ templates. (This would be the first thing on my list of things to fix if I were allowed to make any arbitrary breaking change to C++.)
But like, it seems very wrong to claim it has no values at all: in C++, if you have a function that returns void, you can even call another function that returns void and use the return keyword oh whatever you consider it to have returned. It clearly has some kind of value in many contexts--and, despite having a size that is undefined (and potentially 1), that value carries no state--so there is only one possible value: it is merely the term people use for unit in this kind of language.
(To be clear, though: I am not at all claiming that Rust is wrong for using () or unit or whatever, as there is a ton of history of using such in other safe academic languages and it certainly doesn't confuse someone like me. I do, though, think that its mission isn't well served by not at least trying to look as much like the specific languages that need to be replaced--C and C++--as possible.)
You just are exposed to different languages than what was used as the base.
The comment from fauigerzigerk sums it up well. You just have to know via experience, a great teacher, or very attentive learning that vec![1,2,3] is going to pop all of its values in a "print elements" loop while [1,2,3] will retain each value.
Other languages have similar behavior, but it's often more obvious e.g. Python with the .next() keyword.
This is a nice one ;)
I don't think that's a reasonable take on both the book and C++11/c++14. `Effective Modern C++` is not an intro tutorial, but a book dedicated to trivial and nontrivial gotchas, none of which leads your program to crash. Worst case scenario you just get a compiler error of your own doing, and that's it. I mean, the book covers stuff like the importance of using nullptr instead of NULL, why braced initialization is nice, why and how smart pointers should be used to implement a pimpl, etc. This is hardly mine field territory, and it makes absolutely no sense at all to use these tidbits of all things to justify switching to an entirely new programming language.
C++ is a large, intricate language with features that interact in complex and subtle ways, and I no longer trust myself to keep all the relevant facts in mind. As a result, all I can do is thank you for your bug report, because I no longer plan to update my books to incorporate technical corrections. Lacking the ability to fairly evaluate whether a bug report is valid, I think this is the only responsible course of action.
https://scottmeyers.blogspot.com/2018/09/the-errata-evaluati...
How usable is it ? Is it closer to a a Proof of concept or a finished tool ?
That's a wildly exaggerated take. Working on something to improve user experience and simplify it is not nor it ever was the same as something getting out of hand. This take is even more absurd when we notice that the main goal of cppfront is to serve as a testing ground ti explore backwards incompatible changes to understand if they actually improve the language. Cppfront is the answer to "why not" questions when ignoring constraints deriving from backwards compatibility.
My understanding at the time of publication was that there was no shipping compiler that could deal with his implementation library for the book, "Loki".
My impression of C++ changes since then has not been that they were adding new capabilities, but rather trying to add proper ways to achieve the desired compile time/runtime/memory behavior without resorting to (at least as much) arcane template metaprogramming.
No you don't. A tuple needs to be declared once. Then everything else is sugared.
#include <string>
#include <tuple>
using Foo = std::tuple<int, std::string, float>;
Foo foo(int a, const char* b, double c)
{
return {a,b,c};
}
Foo bar(long);
double baz(long x) {
const auto [a, b, c] = bar(x);
return c;
}
godbolt: https://godbolt.org/z/exdsb4hqbNote that the return part of this (foo()) worked as far back as C++11. Only the structured bindings on the receiving side (baz) requires C++17.
auto get_multiple_values(const bool condition)
-> std::tuple<bool, int>
{
if (condition) {
return {true, 5};
}
return {false, 0};
}
void foo()
{
auto [status, value] = get_multiple_values(true);
}Ehh... not putting a return type in at all is a bridge to far IMO. Be friendly to the casual reader of the code, and help them understand what the function returns. (Yes I know IDE's can do this for you, but not everyone uses an IDE, and not everyone is reading the code in the context of an IDE... for instance code review, etc.)
Wrote this for myself, I’m a vim user :)
Sometimes it deduces "wrong". (As in not the type the coder was expecting, so really the dev was wrong and missed a possible bug)
But if your argument is "this makes the reader do extra work" then that's an argument against all uses of `auto`. Now I do consider reader clarity a legitimate (and important) design criterion (and argument in code review), but I don't think it is reasonable in this case.
In fact I consider type deduction with auto to be an important element of readability: it tells the reader not to worry about the type and focus on the logic. Then the cases when a type is explicitly specified it tells the reader that it's worth paying attention.
And you ignored any value that actually specifying the type can have.
You also ignored the age old wisdom that code is written once and read many times.
You said that people not having an IDE was "bridge too far" meaning that people should always have them. We already had examples of where that simply isn't true so your idea was demonstrated as wrong. Own it get a better idea and move on. I certainly had until I went through my past comments looking for childiah arguments and found one.
For this reason, some prefer a “auto almost everywhere” style, in which you preferentially use trailing return types.
it is known as sticky syntax in relation to auto (but specifically when the type on the rhs is explicitly declared and not abstracted away with a function signature)
it removes narrowing, inference, while keeping benefits of auto related to inits etc.
it's a way of telling the compiler we want this type moving forward and adheres to the convention of right-to-left reading w.r.t. resource allocation semantics C++ programmers are often accustomed to.
Then once that was opened some folks felt it would be better to write their regular code that way too.
auto foo() {
struct Result { int a; int b; int c; };
return {1,2,3};
}
//....
auto [a,b,c] = foo();I love the language overall. Seems like standard & easier management of dependencies and compiler flags would help it thrive again.
The example I always use: you want to use libfftw ("Fastest Fourier Transform in the West"). But the library can be compiled with (at least) two options: use floats or doubles, use threads or not. That creates (at least) four possible variants of the final library.
The only way to address this (and IIUC, Cargo for Rust does this) is to somehow have the pkg manager be a part of your build system, so that you end up with a local copy of the library, built as required.
Meson did that. You can use system packages, define your own dependency, use sub-projects (custom build option a, you mentioned) or readily available from WrapDB.
Kind of an uphill struggle this one, because there's no standard implementation of the language, nor is there a standard build system. You have to deal with multiple vendors, one of whom is Microsoft and many of whom are unwilling to change rapidly.
The dependency declaration and auto-detection is nice. But the hidden extra is WrapDB, built-in package management (if wanted):
https://mesonbuild.com/Wrap-dependency-system-manual.html
https://mesonbuild.com/Wrapdb-projects.html1. The build in step-by-step rather than end-to-end. Meaning that you first build Boost completely, then Qt, and then your application. With an end-to-end build you would build everything with a single build system invocation with the build system "seeing" the entire build graph. In particular, this would allow you to build only what's necessary and faster.
2. If something goes wrong in one of these dependencies, you better be prepare to become an expert in whatever build system it uses.
If you want a true Cargo-like experience in C++, a better option would be build2, which replaces rather than wrapps the build system (and, yes, it has Boost and Qt packages): https://build2.org
For more details on what an end-to-end build can give you, see: https://build2.org/faq.xhtml#why-package-managers
Probably my top footgun for C++ is implicit copy construction by default. Every class gets a copy constructor by default unless it opts out. When a copy constructor exists, it can be used implicitly and silently for casting and other contexts. Implicitly copying big objects like std::string or std::vector can be a huge, silent waste of time - whereas in Rust you must explicitly call `.clone()`.
> const is not the default
Agreed with this footgun. The old C and C++ ways are to be permissive by default, to be strict after adding more words.
As another example, top-level variables and functions are exported by default unless prefixed with `static`. Whereas in Java, Rust, etc., you need `public`/`pub` to export, nothing to keep private.
Another huge footgun is the collection of all the undefined behaviors from integer overflow to null dereferencing to creating out-of-bounds pointers to buffer overflows.
between figuring out CMake scripts, std::optional being basically useless. I never feel done writing code because I don't feel like I have:
1. Checked all the exceptions
2. Checked all the places where my std::optional instance is null (the checks doesn't propagate their information)
3. Checked that I am not doing any implicit conversion.
4. Really understand what happens at overflow?
5. Really understand if my code is even UB free?
6. Debugging is a mess, I can't step through code because the smartass compiler removes an entire segment of code even in debug mode.
7. I wish this garbage is written in C instead. At least its honest that its primitive and sinful, unlike this LARPING high level language.
Only 2 things need to be true for this language to be permanently horrible: Hyrum's Law and backward compability. If it is possible to write bad code, then there will be bad code. Simple as. Doesn't matter how many guidelines you write or how many times you need to "teach" programmers or how many times you said people just need to be "better mkayyy?"
Computer graphics, though, is one of the few sane reasons left to learn C++.
Congratulations! That is the correct answer. Welcome to the language.
If you don't need to use C++, don't.
You need C++ for stuff like computer graphics, computational geometry, plausibly scientific computing, games and so on.
"6. Debugging is a mess, I can't step through code because the smartass compiler removes an entire segment of code even in debug mode."
Visual Studio is brilliant compared to anything else. If you really need to use C++, I would warmly suggest it.
I have to because everything else is in C++ and I don't want to be the jackass that uses their favorite language that only they know in the company.
That's a bold statement. Clion and vs code are pretty great too, and last time i did a comparaison better than visual studio proper.
There are warnings for this that you should activate.
The rest is pretty much incurable.
I know, the post is about C++ and Rust, but I'm tired of this "C++ is abandonware, just switch to a modern language" narrative.
If one cares about language design beyond the ideas explored in Oberon and Limbo, not so much.
I wonder if you are unable to understand that for some people and some problems, having fewer footguns is a benefit.
Lots of Usenet discussions on the matter, and other places.
Have you ever produced commercial software for 8 bit machines?
What's important is the RAM and ROM limitations.
Plus meeting latency deadlines.
Power requirements.
That the program never gets shut down for years.
That there will be no one to intervene when something goes wrong.
Current product I work on has shipped a few million units. If someone needs to replace one that's failed it costs more than the unit itself.
It'd been nice not to use C. Be nice if C had a few improvements to make it more ergonomic and safer.
I see Debian today tinkering with two additional kernels - FreeBSD and Hurd, and both are written in C.
Until some kernel 100% written in Rust/Go/modern C++ starts being attractive for OS integration there is absolutely no point in claiming it's possible and even a better choice.
C has lost a lot of ground since 30 years ago, rightfully. User applications with GUI used to be written in something like C89. Games too. It's some people that just cannot grasp that it and its ecosystem have a huge merit over 'alternatives' when it comes to the ground it retained and still holds today.
But we have started seeing just a bit of Rust support in Linux now…
I think this narrative is still valid. For the love of god, if you don't need to use C++ for a greenfield project, don't. I say this as a person who needs to write C++ daily.
Otoh, if you do need to use C++, it's likely there are no good alternatives, so unless you are Microsoft, Google or Facebook ready to invest few billions to a new language ecosystem, the most pragmatic approach is to, indeed, just use C++.
Chances are if there's a problem to be solved, someone has solved it in C or C++ and the code is out there already.
It also has a lot of great support in terms of toolchains. Almost everything everywhere supports it out of the box, and you have your pick of any tools you want.
It also works on fairly simple principles, so anyone with experience in programming can work in it without learning a completely new way of thinking. [Even if their code won't be amazing at first]
In general I agree with Mark Russinovich. Use a garbage colleted language, and if you really want a non-gc language choose Rust (which I really should learn).
https://twitter.com/markrussinovich/status/15719951172335042...
There are cases when one should choose C++, but those are the exceptions. Graphics, computational geometry and so on are strong reasons to use C++ still. But not everyone needs that stuff.
For moving strings and numbers around in a stereotypical business domain there are much better languages. C#. Python. Etc.
"an enormous pile of easily-usable existing C and C++ code that you can hook in easily into any project."
A lot of it is to help with the fact that the standard library in C++ is anemic.
This is one of the reasons though why one would want to use C++. Some libraries exist only in C++, and yes, it's a good reason to use the language.
"It also has a lot of great support in terms of toolchains."
CMake? VCPKG? Conan? MSBuild? Meson? Scons? Bazel? Make? Xcode? The-android-thingy? How do you mix and match those?
All of those are bloody horrible. And their combinations to the second power so. The only good thing in modern times is that most open source projects have picked up CMake with a fairly standard way of project configuration, which enables actually to reuse other's people code with surprising ease.
The toolchains make a horrible language a doubly terrible.
"It also works on fairly simple principles, so anyone with experience in programming can work in it without learning a completely new way of thinking. [Even if their code won't be amazing at first]"
Calling C++ simple is a new one to me. It's an industrial language that is not a total failure. Yes, people can write programs with it. The problem is there are much safer and better languages to write general programs, thus selecting C++ is giving yourself an intentional handicap in term of value created.
As an example of the complexity choosing C++ over something like C# gives you -
To what extent is a C++ codebase cross compilable and runnable in say MacOS after you develop it in Visual Studio on Windows? That's right, you can't tell, the only way to verify your standard C++ has the same runtime behaviour is to run it on the said platform. And fix all the bugs. So to actually write cross platform code, you need to have some proficiency in all of the platforms to have any level of guarantee that the code actually works in it's intended runtime.
Thus, a C++ codebase needs to come with a warning label telling every OS and processor it has been tested in. Most popular libraries are tested and run in all of the relevant platforms so this is not an issue for users. But I'm pretty sure the number of platform specific fixes per codebase is not trivial to make something work even remotely similarly.
Almost everything I've seen is in Cmake, or can be easily interacted with it.
But in general, just pick something you like and stick with it. Sure, they're not perfect, but this list is actually a point I was making - you have a huge array of mature solutions out there already.
>Calling C++ simple is a new one to me. It's an industrial language that is not a total failure. Yes, people can write programs with it. The problem is there are much safer and better languages to write general programs, thus selecting C++ is giving yourself an intentional handicap in term of value created.
C++ is a simple language with complex things you can choose to use. If you understand the basic principles of loops, variables, functions, arrays, etc, you can write in C++ right away. You can layer on greater complexity with libraries and the STL, better patterns etc as you go, but you can start simple. That's a great benefit to someone learning, IMO.
C++ is as complex as you make it. I prefer to write fairly simple C++ personally and only break out the harder parts when absolutely needed. For example, I really don't like lambdas, so I just don't use them. And I can still solve anything I need to without them.
Installing library is
cargo add X npm install X implementation(“X”)
Show me how to import library in CMake. And then also show me how to import library that doesn’t have CMakeLists.txt file.
> C++ is a simple language with complex things you can choose to use. If you understand the basic principles of loops, variables, functions, arrays, etc, you can write in C++ right away. You can layer on greater complexity with libraries and the STL, better patterns etc as you go, but you can start simple. That's a great benefit to someone learning, IMO. C++ is as complex as you make it. I prefer to write fairly simple C++ personally and only break out the harder parts when absolutely needed. For example, I really don't like lambdas, so I just don't use them. And I can still solve anything I need to without them.
Good luck explaining to team that they can only use subset of C++. Also good luck when advanced C++ dev advocates for feature X and now whole org has to learn about X and its drawbacks. Or when the dev leaves and rest of team doesn’t know how the hell it works.
Sounds to me like you’re writing all of this from hobby or hobby OSS project perspective. Which is completely fine, but doesn’t apply to orgs.
>Show me how to import library in CMake. And then also show me how to import library that doesn’t have CMakeLists.txt file.
I'm not sure how this is a gotcha. I'm not that familiar with Cargo, but I think the equivalent would just be something like:
TARGET_LINK_LIBRARIES(myProgramName Eigen3::Eigen)
in cmake. Not very hard.
Also...why does it matter if there's a cmakelists.txt file? How is that a bad thing?
>Good luck explaining to team that they can only use subset of C++
You learn as you go, like literally any other language. You don't need to understand every single line and function call in a program to make changes or add things to it. As you come across something you need to modify, you learn a bit about the features being used.
Now, if you have a real go-getter on the team who has covered the entire project in the most advanced features used in the most obscure ways, sure, it's going to suck for anyone just starting. But that's a problem again, in literally any language. And not a fault of C++, frankly.
> TARGET_LINK_LIBRARIES(myProgramName Eigen3::Eigen)
Include how the library gets in your build, not how you link it.
> Also...why does it matter if there's a cmakelists.txt file? How is that a bad thing?
Maybe because it gets dramatically harder than having `cargo install x`? Or did they solve it recently where any autotools/meson/whatever gets pulled into project without any blood sacrifices to pass all compiler flags properly?
> You learn as you go, like literally any other language. You don't need to understand every single line and function call in a program to make changes or add things to it. As you come across something you need to modify, you learn a bit about the features being used.
How can you make you changes to something you don't understand and make sure that it still works? Also, this is absolute nightmare from perspective of team and org cohesion.
> But that's a problem again, in literally any language. And not a fault of C++, frankly.
No, it's not. Aside from maybe Scala, I can't think of any mainstream modern programming language where you have to actively create your own sublanguage to make it bearable.
Kind of depends. If you're in a Linux environment or something like MSYS, you'd just need the relevant Eigen package (in MSYS: mingw-w64-eigen3), which then you can link against. You could build it from source instead I suppose, and it has its own cmakelists which you just add as a subproject if you want to go that route.
This isn't rocket science. Could it be easier? Sure. But it's not terrible either.
>How can you make you changes to something you don't understand and make sure that it still works? Also, this is absolute nightmare from perspective of team and org cohesion.
You learn about how the part you're modifying works. Ask for some assistance from the team members. Read the docs. Look up anything from libraries/stdlib that you don't understand, go into the debugger and watch as the state changes. Build the knowledge gradually.
The next thing won't be as difficult. Don't try to take in the entire project at once, that's an overload of information for any non-trivial project. Again, that's true in literally any language.
I implement and maintain C++ for my day job so sure, it's not that bad ... it's only bad when you compare it to the state of the art in other language ecosystems.
"This isn't rocket science. Could it be easier? Sure. But it's not terrible either."
I don't know how to measure "terrible". I agree it's not rocket science. And it's much better than two decades ago.
But still, compared almost to any other industrial language (which of course have their own set of problems) the default experience setting up and maintaining a cross platform project is abysmal.
By "abysmal" I mean that defining the build is not a project goal in itself that requires meticulous design and expertise, and is something that just works out of the box.
If you work only in posix land the experience is far less uncomfortable, of course.
C#, TS if you're looking outside of JVM.
Probably it's mostly about runtime/libraries than language itself.
Aside of embedded applications and hardcore number crunching (core part of it) I can't find a single use for C++ in 2023.
Reference parameters are awful; the reader has no idea whether the argument will be modified or not without reading the header file.
With const pointers you never have this problem.
I find this all a bit rich anyway. In languages like C#, everything is a bloody reference and this lame concept of "reference types" corrupts developer minds.
It is true that C# has value types that are by default passed by copy, though.
When I'm navigating code, the references aren't a problem, because I am using an IDE of some sort, usually.
The complaint I made was about reading, not navigating, for example when reviewing a PR.
Class types are effectively pointers that are passed by the value of the pointer.
You cannot implement swap in Java without eg wrapping the params in one element arrays.
Hardly any different from "var" parameters in plenty of languages since the 1960's.
I found that to be a rare circumstance in any complicated code base. And once you lose that capacity, oh boy does this language become terrible fast.
Visual Studio works fine for millions of lines but I am sure the difference in experience can vary wildly.
In general, the company moves forward because most of the same people who established the code base are still working here and they just already know all the quirks and can Intuit argument types, often because they wrote all the classes in question.
No, we don't spend money on code editors around here; I meant VSCode.
Not spending money in tools sounds like a very strange economics for software development. If interviewing does not scare you I warmly suggest finding a new employer.
Nevermind that I have some experience with how Google did it, and it turns out there's a lot of ills you can solve with a 25,000+-strong engineering force that you can't in a smaller org. VSCode at Google never worked for me either, but punching a novel symbol into company-wide syntax-intelligent internal code search and glancing at the first response, which was likely to be the right one because it was a Google search engine so had well-tuned signals for "things humans care about," always did.
They're not interested in how Google does it, merely that they do it so you can't get fired for making an architectural decision that worked for Google. Nor are they really interested in solving the tooling issue because it works for them; they just grep-and-pray. They're very fast at it because it's all they know so they've gotten quite good at it (when they have to do it at all; often they just know the types because they wrote the classes. ;) ).
Besides the issue of C++ being just hard to compile (global symbol mutation via the macro system, the way templates instantiate, the compilation unit as the fundamental element of compilation meaning any individual compilation unit can require hundreds to thousands of include files as input), the language is rife with undefined behavior and assistive tooling is allowed to crash on undefined behavior. So, yes, if your tooling crashes it may indicate a problem with your code---but how are you going to find it now that your tooling has crashed?
We've had a couple of initiatives at my organization to try and get tooling operational across the code base. They haven't stuck. Developers keep having to fall back to grep and pray.
I prefer working with languages where there are simply fewer features and less undefined behavior. It makes the tooling more reliable.
This organization has its work to do. My advice to other teams starting out is "C++ may be cheap in terms of hiring people and libraries that exist, but consider the cost once your codebase gets large. This is not a language that lends itself to safe behavior or tool-comprehensibility, and if that's something your project needs, be prepared for that. Other languages are safer, other languages are simpler for tooling to digest."
So yeah, difficult-to-parse codebases are not an exclusive feature of C++.
I'm medium-level confident there's meat on the bones of optimizing it to a point better than the C++ tools because they made choices in the language that do some amount of compartmentalization, so you don't get that "one #define up in this header you've never seen changes the meaning of symbols in every other file in this compilation unit." Unless that is possible and I simply haven't gotten deep enough into Rust to realize it yet.
Back in the day, I once worked with a C++ game engine where the developer was obsessed with interfaces vs. implementation. Every class, even if there was one implementation, had an abstract interface wrapper. It was the first codebase I ever encountered where the compiler (on my Mac laptop) choked on it because it ran out of RAM trying to compile it.
While I would certainly prefer the reference to be visible at the call site (a mistake Rust did not repeat), if I have to choose, I prefer the inconvenience of having to use an IDE like qtcreator that will display references in a different style, to introducing an invalid value that needs to be handled (and will be mishandled).
So yeah, as a senior dev, it checks out.
So? It still helps me with decoding `foo(bar, baz)`, as without the `&` I KNOW that bar and baz would not change after the call. And since both bar and baz are in the local scope, I already know whether they are pointers or not.
With pointers, all the information necessary to determine if bar and baz would change after a call to foo is in the local scope. With references that information is elsewhere.
Also the “problem” you describe is not solved by your proposed “solution”. A const pointer can point to something that can be modified.
You can declare a pointer to a const object, or, you know, declare a const ref.
_Success_(return) //out parameters should be set if this returns true
bool tryGetValue(
_In_ Map const* map, //pointer is for input only and cannot be null
_In_ char const* name,
_Out_opt_ Data* value //pointer is for output and can be null
);
If you pass null for _In_ params, the compiler will warn or error. You return true without setting the _out_opt_ parameters, the compiler will warn or error. You can pass null to _out_opt_, and the compiler will warn or error if the function body doesn't properly check if the parameter is null(Although this is something I'd 100% want C++ to do, and not with an annotation but with a very short operator or reserved word.)
> A pointer can be nullptr while a reference cannot so that would be a step backwards.
Backwards from what? A reference can't be null, but it can still be invalid, which is a more common problem than forgetting to check for null-ness.
> You can declare a pointer to a const object,
Yes. I should have said that instead.
> , or, you know, declare a const ref.
That doesn't solve the original problem with references, though. I hate reading `foo (bar, baz)` and having to dig into the header to find out that bar or baz or both might be modified after the call to foo.
When reading code, I want to be able to skip over anything that does not modify the state of variables in the current scope.
Using references means that I have to know what every single call does. Using pointers exclusively, I can simply skip calls to functions with arguments that aren't pointers.
no, a reference cannot be invalid. If you are given an invalid reference (such as referring to an object after its lifetime ended, or referring to something that is not an object of the reference's type), then the fault lies on the caller. The callee cannot check the reference's validity, nor it is its responsibility to do so.
This is *very* different from the case of a pointer, where the caller is not at fault, typesystem wise, for giving a nullptr to a function accepting a pointer.
Increasing the probability of runtime errors to gain a little bit of readability at the caller's site is not a good tradeoff.
With const references you never have this problem.
Const references are less offensive, but I'm not going to pass non-const by pointer and const by reference, that's just inconsistent for no reason.
If null is not a valid value, throw an assert at the beginning of the function. It's not like references can't be in a broken state, they're just syntax sugar for pointers (that also happens to wildly complicate the language's type system)
Why use pointers at all? Just use const references for immutable stuff, regular references when you want mutation, normal values when you want copy semantics, and smart pointers for other cases (unique_ptr for move semantics, shared_ptr for lack of clear ownership, etc.) If you want to represent nullability, std::optional is a good choice.
> If null is not a valid value, throw an assert at the beginning of the function
Yikes. This means the compiler will not catch it for you. Why make this a runtime issue? Use the type system to your advantage.
As for smart pointers there's a lot of domains with tight performance requirements where you can't afford them - and to be quite frank, if you can afford to use shared_ptr you can also afford GC, so just use a sane language
How? Honestly curious. (You can certainly reinterpret_cast something and ignore all warnings, but is that all?)
It sounds like the problem is not using an IDE.
Use references whenever posssible. Then you don't need to do a null pointer check. C++ is a footgun. And like with all guns, security first. With guns - put the safety on. With C++ - check the pointers are not null.
OFC one can argue that with proper instrumentation invalid pointers are captured sooner than later, but the fact is it's much easier to represent invalid program state using pointer than references.
There is no perfect solution. Avoid out parameters if possible, otherwise make the semantics clear via function naming or at least parameter naming conventions.
#define $
And add the $ in front of all non-const pass by reference parameters in the callActually, I'm curious if somebody is working on a flavor of the modern C++ that abandons backward compatibility ?
The Carbon project feels like a completely new language. What I'd expect instead is something that adopts best features and practices from modern C++, so that an average C++ project would require just minor changes to migrate, in spirit of Python 2 -> 3 migration.
This is just Rust. You can't really deprecate bare pointers in C++ without completely wrecking the language.
And all their header files. That's a real Year Zero moment: you've just cut yourself off from the standard library, as well as most of the code you might want to link to. I don't think there's any "simpler" about it.
(I could be wrong though! I follow the committee more than you may guess, but not as much as to think I know everything about what's going on.)
Carbon is a completely new language, and that's the point. Part of the language design is making compilation a lot faster, for example, as well as features that C++ can't realistically implement, while retaining C++ interop. It's an exciting way forward if they manage to achieve their goals, and it would be a way to gradually migrate C++ projects.
I think the best practice is for people to stick to a subset of safe patterns in mature languages. For C++, that's probably the C++ Core Guidelines:
I reject the notion that C++‘s “maturity” is a substantial advantage given its unsafety.
I feel the same whenever I have to go to other languages after using TypeScript. I want to model all of my data using the limited algebraic data type operations that many newer languages have (usually sum and product types).
Recently I've been writing some Go for a project that is a tiny bit beyond a plaything. I am surprised how much I actually like Go and I am very glad to have come to the language to use for real after they added generics. But several times I wanted to do a union `SomeType | AnotherType` and the fact it is missing from Go is like an itch that can't be scratched.
I would also add that discriminated unions alongside type narrowing (usually based on branching or escape analysis) just seems like something I want everywhere. Add in a powerful `match` statement and some kind of destructuring and I start to get very interested.
If there is some future where Go picks up sum types, adds some kind of support for discriminated unions and type narrowing based on branching - then it might just be the best general purpose language (although unlikely to be a contender for best low-level systems level language).
While I agree to a point, I also understand the argument against this. In programs, a lot of state needs to be changing. Which means you're going to type mut a LOT over the course of a 100k line program. And frankly, that gets tedious really quickly.
If you miss a const in C++, no big deal. The program still works. It's mostly a safety net. [with notable exceptions] But if you forget a mut in Rust, the program is not going to work.
That's an advantage (you can't write something incorrectly) but it's also tedious, imo.
When working on the C++ codebase daily, I occasionally find myself going "Hmm, that could be const/I need const here to use something" and swap it over. But it's fairly rare, and overall less time investment on my end.
Frankly, this explains most differences in Rust and C++. Rust is OK with the developer having to do more boilerplate, more verbose code, etc. for the sake of ensuring correctness. C++ assumes the best and goes with it to save some time in the short run. That's neither good nor bad, just preference.
I would argue that one of them giving you a better chance at producing reliable software should go above and beyond the notion of preference.
It may not be as large a proportion as you think. Doing a quick grep on the rustc compiler source (483k SLoC), there are 5431 matches for "let mut [\w\d_]+ =", and 29520 for "let [\w\d_]+ =". This is, of course, is only simple bindings and misses out on more complex patterns along with if-let and match expressions, but even so, about 84% aren't mut bindings.
One of my own codebases (including all its Rust dependencies is about 1.5m SLoC) comes to about 80% not being mut. Ripgrep + dependencies (718k SLoC) is about 77% not mut.
I just can't take your argument seriously. One of your arguments is "the program will still work, with notable exceptions". I'm not persuaded.
You say it's a matter of preference, fair enough. I say let's make companies financially liable for the consequences of their preferences and see how this shakes out.
I can't think of anything significant off-hand, it was just more to stave off this part from the post:
>If you take a parameter by non-const reference, the caller can only use lvalues to call your function. But if you take a parameter by const reference, the caller can use lvalues or rvalues. So some functions, in order to be used in natural ways, must take their parameters by const reference.
>I say let's make companies financially liable for the consequences of their preferences and see how this shakes out.
This is completely unrelated. It's also a bit of a dangerous road to go down and I'd suggest thinking about it a lot more.
[0] The compiler will gladly pass a non-const pointer to a non-const object to a function that expects a const pointer to a const object; it all "makes sense". However, a compiler will explode when you're trying to pass a non-const vector of non-const objects to a function that expects a const vector of const objects.
The elements of std::vector cannot be const.
I suspect a certain laziness in not tackling some of the harder problems in the compiler, for example: type inference, improving the grammar to disambiguate new keywords from variables (for example co_yield). Many features could have been improved with more effort and willingness to tackle hard problems.
Perhaps if Sean Baxter's Circle (C++ language extensions) gains traction, there will be hope to modernize C++ syntax and features.
* You can just write = default for those methods often enough, and having to implement these is extremely rare
* use [[nodiscard]] for your silly bool get_message return
* C++ does in fact support multiple return types via structured bindings, or just use a struct
None of this is really what is wrong with C++, the real issues are that everyone uses a different build system and a different way of sharing code.
And for tuples, you still need to use std::tie or std::make_tuple on the other end.
None of this however precludes me for writing well behaved software using subset of modern C++ that I actually need. The good thing is that when I need some esoteric feature that will help me to to this and that - it is always there and is quickly searchable thanks the the Internet.
I prefer wide array of C++ choices especially when it comes to supporting various paradigms and techniques to a stricter set in Rust for example.
I do see that real C++ experts (I am not the one) can get frustrated with the language but I think we have different goals in general. Being a programmer I do run my own business and I believe ROI wise C++ is more beneficial for my situation (I do use multiple languages when needed / requested by clients).
> It is unergonomic to return multiple values by tuple in C++. It can be done, but the calls to std::tie and std::make_tuple are long-winded and distracting, not to mention that you’ll be writing unidiomatically, which is always bad for people who are reading and debugging your code.
C++14 allows you to move capture in lambdas. C++17 has structured bindings that help with returning multiple values.
If you are claiming to an expert in C++ talking about paper cuts, at least be aware of things that have happened in C++ in the last decade.
Your quote says cpp11 and youre arguing with 14? Wtf?
Source: Someone who was, until recently, limited to C++03. Luckily we're up to 17 now, but occasionally 11 limitations creep in.
In any case, I revised that section to explain how even the "fixed" version in C++14 is still a paper-cut, and make the section hopefully flow a little better.
ETA: I replied to the right comment. I'm agreeing with you :-)
I was just saying that being stuck on older versions is more common that most people think.
"Lambda capture" sounds like "my favourite way to juggle chainsaws".
The correct answer to "I don't like lambda syntax..." is not "learn lambda syntax" but "do not use lambdas unless you really need them, and then think twice".
Honestly, C++ is a massive footgun. Write the most simple, inelegant C++ you possibly can. The coming generations who need to maintain your code for the next 200 years will be thankfull for it.
Don't pretend C++ is something more elegant like Ocaml that the compiler sugars you into believing. It's not! It's a horrible pile of ... things ... piled .. on top of .. things .. on top of ...(etc).
And when something goes sideways at any point in that pile of templated tower of abstractions, the compiler will gift you with an outpout that is closer to a mix of toenail clippings and dog poo than an output from an industrial level programming tool. After which you need a heavy drink and wish there was a better language out there. And then you realize there isn't, for the specific task, and you adjust by reducing the complexity of the abstractions you use and prefer to have a codebase that has more lines of code and fever chances of irritating the compiler.
It’s not about the syntax, it’s about understanding the resource management model lambdas add on top of the existing mess.
There is no good reason to use lambdas except parametrization of stl algorithms.
Don’t use them as part of api design. The only thing you gain is one more level of complexity on reasoning resource management.
Yes, to be specific, one should know how they work, and then not use them.
This is in the context of C++ being a minefield of practically infinite complexity, in which the only way to survive is to pick the language subset you know you are able to handle, and stick with it.
Of course, every party picks their own subset :)
Lambda-the-c++-language-feature is not. The purpose of programming languages in an industrial context is to make implementations of programs simple. When they utilize complex abstractions, the abstraction should simplify overall architecture. The way to measure the ‘simplification’ is how much reasoning a programmer needs to do to understand what their code does in the context of the language constraints and runtime.
In the context of C++ this includes runtime memory use and execution speed. The latter requirement means C++ code must be such that it’s amenable to performance analysis and when hotspots arise, one should have an obvious strategy how to continue.
C++ Lambdas make this reasoning more complex (in my experience).
So the main problem is the added complexity to reasoning about runtime behaviour, resource use and performance.
And this is not complexity that would add value by removing that complexity from some other area of the program.
Lambdas in C++ start by looking cute and innocent. Then, when the first problems arise, you realize that by using a lambda instead of just an in-place class to capture the context, which you can reason using the same bysantine set of rules, you actually need to grok a whole new set of rules on top of that: https://en.cppreference.com/w/cpp/language/lambda
That’s a huge mental cost in general what is a saving of few lines of code. Hence, don’t use lambdas in api design.
The only way I guess to realize why the lambdas are in such bad taste is to use a programming language where they are part of the core language syntax, like python, scheme, F#… where you can use them without each time double checking that you are not accidentally shooting your foot off.
The question is - what does this extra complexity buy us? Is it worth it?
Functors are no easier to use. Either you initialize captured values through a constructor or by manually setting each member. In both cases you have to name the same values several times and ensure that you're doing it correctly. With lambdas, if capture by value or by reference is enough for everything you need, the code is much more succinct and readable. It seriously reduces useless, error-prone boilerplate.
>And are the runtime guarantees exactly the same as that of a local class instance?
Yes.
>The question is - what does this extra complexity buy us? Is it worth it?
It buys us more readable and straightforward code. Yeah, I'd say it's worth it. I haven't written a functor in years.
No, I don't agree at all. Unless the callee accepts an std::function, passing a lambda is exactly equivalent to passing an ad hoc functor. There's no problem you'll run into with one that you wouldn't have run into with the other.
IIRC, there are usually several alternate ways to parameterize STL algorithms. If lambdas don't add enough value to be worth the additional complexity in other places, then they don't as STL arguments either.
And I have now addressed structured bindings in the article itself for what they are: a halfway solution, unacceptable to anyone used to real tuples.
Stopped reading here. std::variant and std::tuple.
When people talk about first-class support for tuples, std::tuple does not fit that bill.
Edit: It's really important to read criticisms of your favorite language. Often, it can help you find a better language for the task you're trying to accomplish.
ETA: std::variant is one of the least ergonomic user interfaces I've ever seen. std::tuple seems great until you realize that most PLs that support tuples just do "(,)"s for them, in creating, binding, and naming the type. C++'s versions are just unacceptable, and structured bindings only help on the binding side.
First-class support for these is important. There's no reason they should be in the standard library rather than built into the language itself, besides historical mistakes.
This C++ code compiles and does what a casual reader would expect it to do:
#include <string>
#include <tuple>
using Foo = std::tuple<int, std::string, float>;
Foo foo(int a, const char* b, double c)
{
return {a,b,c};
}
godbolt: https://godbolt.org/z/Koa4zYT99I gotta be honest, this interface is exactly what I would like a tuple API to look like. I want to declare my tuple as a `tuple`, I want to give it a friendly name, and I want to explicitly declare the types in the tuple, and I want to all of that at compile time. This API is exactly the verbosity that I want; I would be unhappy with either more or less boilerplate than this.
> First-class support for these is important.
Agree to disagree.
Why are you assuming I don't use them? Sum types are great.
How do you know correct? Very few pieces of software are subject to full theorem-prover correctness, and very few pieces of C++ are completely free from ever invoking "undefined behavior" at runtime. Because that's extremely hard to do when even "x = x + 1" can be UB.
Don't hold C++ programs up to a standard that programs in other languages also can't pass.
Think you have the wrong person? I have no idea what this refers to.
Back to the larger point: How many Rust programs meet your proposed level of "correctness"? How many Fortran programs, Java programs, Python programs? Virtually none of our software does. Why hold C++ to a standard that nothing else meets either?
Each probablility of a mistake may be low on its own, but they compound with every avenue to make mistakes.
"Rust makes a few classes of bugs impossible to write by construction" is a very nice feature of the language. But I'm really turned off by how insufferable the Rust community is (which shines through in the OP's blog post and several comments elsewhere in the responses): it's never the quote at the start of this paragraph; it's always "writing software in anything but Rust is fundamentally irresponsible!" or "if a Rust program compiles, it must be correct!" No, not even close.
This is very well put and mirrors my sentiment as a long-term C++ swe.
I also think that the following quote is rather misrepresentative:
> C++ is a great programming language. The complaints are just from people who aren’t up to it. If they were better programmers, they’d appreciate the C++ way of doing things, and they wouldn’t need their hand-held. Languages like Rust are not helpful for such true professionals.
C++ is a great language and a lot of complaints do come from people who aren't up to learn it properly. This isn't elitism, and this state of things has been true for every skill-based discipline in history. I am not saying that I am better than people who refuse to learn C++, or that C++ is a better language than others - I don't think there are many C++ programmers at all who do, it would be a very ridiculous stance.
C++ is a great language, it is also a somewhat dangerous language. It is not a language for everyone and every purpose. And that is fine. Just like knives are great instruments even if they are a bit dangerous and unsafe to those who do not wish to learn it properly and where it makes sense.
At the end of the day, it doesn't matter how many people who just don't see a problem with C++ one calls "elitist" or other things, not everyone is obsessed with micro-improving a language that clearly works very well.