Funnily enough, when I joined a C++ team about 4 years ago I could never get any senior dev to move away from using references instead of const pointers.
Reference parameters are awful; the reader has no idea whether the argument will be modified or not without reading the header file.
Use a decent IDE. Most render & at the call site so you know.
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.
I'm pretty sure one of the biggest differences between C# and Java is that in C# you can't assume everything is a reference. A strict is passed by value.
No, strings are passed by reference, it's just that the string type is immutable. Passing a string to a function doesn't create a copy, but there's also no way for the callee to modify the object that the caller points to.
It is true that C# has value types that are by default passed by copy, though.
Visual Studio crashes on the code base I maintain. It's a monorepo using bazel to manage it, and between the sheer number of includes compilation units take, the hundred thousand plus compilation units themselves, and some decisions made early on that are hostile to VS code tooling because the original developers didn't use VS code (such as hiding template implementations in .inl files that are included in the corresponding .hh file), VS code is functionally unusable for us.
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.
Visual Studio Community is free to any company whose annual revenue is under $1 million USD. It's fully functional, unlike the ancient Express versions.
"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.
Visual Studio is not visual studio code. Also if you have a hundred thousand compilation units, it might be time for some shared libraries instead of one monolithic program.
Oh I completely agree, but I am a mere senior software engineer and I have not yet figured out how to chisel past the corporate cruft of the three folks in charge of the codebase who believe monorepos are right because Google is doing it, therefore it must be the best way to do things.
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. ;) ).
And they are, indeed, very impressive. But the nature of the language makes their task extremely difficult, and in my experience they aren't reliable.
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.
Indeed, although I don't know what we could throw money at that would fix the problem of "Clangd crashes trying to provide LSP services for this codebase." We could buy Visual Studio and roll the dice and hope that it's more robust than vscode, but I have no particular reason to believe it would be.
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."
I recently worked in a fairly large Rust project that I could simply not load into VS Code. rust-analyze would allocate more and more memory until it eventually crashed. I have never seen that with C++ code, besides that time I was investigating how someone was using CImg and found out that the CImg developers thought it was cute to make a single 3 MB header, and that brought the debugger to its knees. It still didn't crash, though!
So yeah, difficult-to-parse codebases are not an exclusive feature of C++.
Interesting. This is the first I've heard of a Rust codebase too big to parse, but I'm not deeply surprised; Rust tooling hasn't been hammered on for as many decades as the C++ tooling.
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.
Funny you should mention that. I'm pretty sure the reason this project was so difficult to process was the (IMO) abusive use of macros. I'm talking foo!( in one line, and several hundred lines below was the closing parenthesis.
Macros (and in C++, their neighbor templates, since both are ways to make conditional decisions about what code to generate evaluated at compile time) are definitely the way to test out the limits of the compiler's performance. I haven't done enough Rust yet to hit this, but I don't doubt it happens. I did once make vscode scream on a very intricate family of conditional TypeScript types... Code compiled fine, but the editor started to take dozens of seconds to determine on-the-fly if types were sound.
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.
with const pointers you have the different problem that you need to define what happens when the nullptr is passed.
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).
In the absence of IDE features, you can't differentiate between value arguments and reference arguments unless you view the callee's prototype. Pointers do not have this problem.
In the absence of IDE features, you also can't differentiate between a function taking a pointer and a const pointer. You must be aware in all cases that the value that you pass to the function may be modified. Yes, the function would not be modifying the pointer itself, but the outcome is exactly the same. A reference is pretty much equal to a non-nullable pointer in this context.
> In the absence of IDE features, you also can't differentiate between a function taking a pointer and a const pointer.
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.
Microsoft created SAL so you can annotate pointers to instruct the compiler to put up warnings or even errors for null required fields:
_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
Of course, that only works if you're passing a literal null. If you're passing a variable the compiler will not check anything, so this doesn't really solve the problem.
The annotation does need to be propagated backwards to callers and declarers - and there needs to be a way to "annotate" an existing pointer in the scopes where you've proven that it's not null - but that's better than having no annotation at all.
(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.)
[EDIT: Brainfarted in the last paragraph, read "const ref" as "ref"]
> 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.
> 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.
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 pointers, you have no idea if the parameter is implicitly optional, if it must point to a valid object, if it will throw an error when provided with a null pointer, if it will segfault when provided with a null pointer. Not even reading the header file will help you here.
With const references you never have this problem.
Yes, but non-const references are utter trash since you can't tell you're passing a mutable reference when looking at the call site, so for the non-const I'll always pass by pointer.
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)
> 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.
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.
If references were guarenteed sane and consistent values like in Rust, sure - but it's trivial to get a bogus reference in C++, so I don't buy the type safety thing. An assert actually catches errors, a reference just declares intent.
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
Yeah, I’d beg to differ there. Non-const references have all the non-nullable benefits over pointer-to-non-const as const references have over pointer-to-const. While I’d prefer a call-site syntax difference similar to what Rust has, the non-null ability of references is well worth it.
IMO read the damn header file and abide by the api contract. A const pointer type suggests nullability, and possibly even pointer arithmetic is permissible.
"get any senior dev to move away from using references instead of const pointers."
...
"the reader has no idea whether the argument will be modified or not without reading the header file."
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.
If you pass a pointer (or anything with reference semantics) by value you can still get non local mutation through it without obvious call site syntax.
There is no perfect solution. Avoid out parameters if possible, otherwise make the semantics clear via function naming or at least parameter naming conventions.
With regards to multiple return values in "By-Reference Parameter Papercuts", you can make it more ergonomic with initializer list and structured bindings without having to use std::make_tuple and std::tie:
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);
}
> add a postfix type if inference can't do the job for a reason or another
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.)
Ehh… not using an IDE is a bridge too far IMO. Be friendly to the casual writer of the code and help them express themselves without having to repeat the obvious.
(Yes, I know, we write less time than read, but maybe that is what is actually wrong with the tools we use?)
Those cases are the ones for which the explicit return type was intended. And if the deduced type is counterintuitive you could also use the explicit return type (or a comment: there are good arguments for either choice).
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.
Mandating or expecting tools for others is a problem. Many code reviews are done in web settings, and that was an example you ignored fromt he person you responded.
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.
Why are you attacking me? I just put some motivation for myself and maybe someone else to switch to an IDE.
Is this mandating tools thing a problem where you are working?
How did that affect you?
Why do you feel attacked? You aren't your ideas, and in this case you had a bad idea.
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.
Ideally, functions should be clear at the call site without a reader having to jump to the declaration to understand what's going on, and you don't get the return type there in the first place.
Not OP but I prefer trailing return type syntax if I were designing a language from scratch... I sometimes wish C++11 would have added the `func` keyword as an alias for `auto` in function declarations.
This is a stylistic difference, as there are some places where the leading return type is invalid, but a trailing return type using the -> is valid. For example, an immediately invoked lambda expression in which some of the returned values require type conversions.
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.
I'm just spoiled from being able to return and consume a tuple in Rust without writing a standard library function or type name once :-) I'm glad C++ has got it down to one though! Now if only people will start using this instead of out parameters (unlikely...)
I don't know. I switched to go a year ago after 10+ years of C++ and, if anything, I realized that C++ was not so bad. Not bad at all. It's old and overly complicated, but it's also very powerful and even beautiful at times. Go is just bad, the runtime is great, but it's attached to a slightly upgraded version of C. It's not even much safer, I've seen more leaks in this year than probably 5 years of C++ before that.
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.
Then those outside groups should make a software foundation that's attractive enough to replace UNIX.
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.
Doing a top down rewrite of an OS is an absurd task, so regrardless of merit, C is going to retain its place there by momentum. If it’s going to be replaced at all, it will be incrementally, and until recently, the powers that be have kept other languages out of that space (and for good reason, since multiple languages interacting have complexity costs, again regardless of individual language merits).
But we have started seeing just a bit of Rust support in Linux now…
'"C++ is abandonware, just switch to a modern language" narrative.'
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++.
As a C++ dev, I definitely disagree. C++ has a lot of advantages, like an enormous pile of easily-usable existing C and C++ code that you can hook in easily into any project.
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]
I must apologize if the below sounds acerbic. I've used C++ professionally close to two decades and developed a keen feeling of everything that is bad in it. (Still need to use it though).
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).
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.
>Make? VCPKG? Conan? MSBuild? Meson? Scons? Bazel? Make? Xcode? The-android-thingy? How do you mix and match those?
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.
> 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.
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.
I'm writing this from the experiencing of maintaining large legacy C++ projects as well as developing new ones. I only do this for work, not as a hobby, nor anything OS.
>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:
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.
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.
>Include how the library gets in your build, not how you link it.
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.
It sounds like you are operating in an existing org with a mature codebase. In
this context, where the existing product is implemented using C++, continuing to use the language makes perfect sense.
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.
Java has incredible tooling, far more advanced than whatever exists in C++ world. If you're looking specifically for "fairly new" language then Kotlin is your bet. Tooling is almost on par with Java. Can utilize ALL of Java ecosystem on JVM. Multiplatform (JVM, JS, native).
I think this is a fair view, but C++ has a lot of flaws. It's good enough that it's very hard to justify switching away from it, and it will be a good resume language for quite a long time, but I would hesitate to start any new project in it.
Interesting, it's quite the opposite to me. I've been in C++ programming since TurboVision times (don't miss them), built many projects in C++, but Golang feels like fresh air to me, at least for server applications.
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.
Starting from scratch, Rust is a fine option there, but it lacks publicly available platform integrations, or any engines as powerful as Unreal. It’s to be seen if that momentum advantage can be overcome anytime soon.
I never recommended Go :-) I'd personally rather program C++ than Go, so I can see where you're coming from! That said, C++ is abandonware, switch to a modern language :-) :-)
Back in 2015 I read Effective Modern C++ by Scott Meyers [1], which is his latest entry in the saga of "Effective C++" books. It covers all the new stuff that got into the language with C++11 and C++14, and shows how modern C++ can be written.
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.
Yeah, Effective Modern C++ has always read like a list of warnings. C++ is kind of like Python in that the design of the language does not steer you into making good, idiomatic decisions. No one is going to guess most of the stuff that is taught in that book without reading it first from working with the language or reading other people's code, and really a full list of such things for C++ could be many hundreds or even thousands.
I also agree that the new features seem to embrace and extend the "footgun-ness" of C++, rather than attempt to fix it.
> the design of the language does not steer you into making good, idiomatic decisions.
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.
I don't agree. I can think of many languages that obfuscate their idioms that have bounded applicability. Obviously I already mentioned Python, which is obviously bounded to less performant applications (unless you call C code), but another one is SQL. For example, Azure SQL is extremely slow when using sub queries compared to CTEs, but the language does not make this apparent in its design.
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.
One of the main places I see idioms in other languages is in dealing with containers (and there are some very nice ones). Python in particular, but others too. Slicing, splicing, iterating, mutating, map-reduce etc. etc.
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.
If rust can do it so well, I see no remaining argument about performance and control. It was long believed that C++ had to be bad in this way because such languages must be. Rust was the counter example that ended this myth
That's not clear to me. The examples I've seen from anywhere that Rust has to deal with hardware suggest that you have to throw out most of the safety guarantees of the language, and this applies also to dealing with OS-provided abstractions of hardware (like MMU-mapped registers and mmap-ed memory regions).
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.
OK, let me put it differently: you cannot reason about safety in those circumstances in the way you can when "unsafe" has not been used. And since reasoning about safety (or perhaps more accurately, not needing to) is one of the raison d'etres for Rust, that's a bit of a hit. Not huge, but for those of us close to metal of some sort, it's a bit of a hit.
Yes, you can reason about safety in just the same way, as long as a safe API is built around the unsafe usage. That’s why you can have safe OpenGL bindings even though the underlying raw C calls are unsafe
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 gap in my knowledge here is how well Rust can take advantage of per-architecture optimizations that can only be seen in the large.
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. ;) ).
No kidding. Effective Modern C++ was what got me to actually start learning Rust.
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.
"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.
Rust would be so much easier for me if it didn't feel like every. single. keyword. was different than my daily drivers, Python and C---from type names to standard library functions, containers to punctuation. Even "void" isn't safe (hehe pun) from this keyword purge.
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.
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 find Rust extremely unreadable and can detail exactly how:
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...
I've been leaning Spanish for about the last six years. I'm reasonably effective at it. Not fluent, but I can speak and understand it well enough that I often don't notice how easy it's gotten for me. A while back, I tried reading something written in German and was actually surprised at how little I understood. Clearly I can understand not-English, why was German so hard? I spent a little while trying to parse out each word, guess common roots, vaguely remember the little bits of German grammar I've absorbed over the years, and so on. It was illuminating. I could not just force myself to understand German by relying on my success learning Spanish. (This is a real story, btw. I really did this.)
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.
My mom was a Spanish teacher in a reasonably hispanic part of Florida. I'm now a permanent resident of Germany. Your example resonates.
-----
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.
> I spent a little while trying to parse out each word, guess common roots, vaguely remember the little bits of German grammar I've absorbed over the years, and so on.
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.
> Clearly I can understand not-English, why was German so hard?
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.
This self taught Mexican genius had an amusing description of ML languages.
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?
Do you expect there to be an easy analog between any two random languages? Even two languages as similar as javascript and python I would not expect to immediately jump in and understand something written in one by just kinda guessing as to what something I didn't recognise meant, or assuming that just because something is written the same as in one language it means the same as in the other.
Yep. Honestly, I don't find it too difficult to parse new languages even if they aren't C-like or Python-like (or Matlab-like or BASIC-like). I don't know Java, but I can read the Falstad circuit simulator source. It's just... very recognizable code patterns---almost a dialect of C++.
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.)
Well there it is. Rust is difficult not because it's inherently difficult but because it's not the spitting image of the languages you've learned and you're not interested learning (which is totally fine). Things like putting dependencies (instead of program source) in 'modules' seem intuitive not because they're inherently intuitive or common but because you've taken the time to learn Python.
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.
Oh interesting, I'd never seen that before. However I'm not a monorepo kinda guy and to me the ripgrep repo already looks way too busy. By the time you've enough crates to shove them in a 'crates' directory I think it's worth splitting them up into different repos.
Cargo is quite bad at true monorepos, so in some sense I agree, but I think there’s often a sweet spot between the two where this makes sense. Some projects are just inherently larger, and breaking them down helps, and spreading the repos out hurts. Just depends.
Blech, no. That would make the maintenance burden of dealing with those crates even worse than it already is. And just imagine having issues spread out all across different repos. Oh my goodness. It would be absolute madness.
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).
This is really helpful to understanding what you mean. If you would be interested, I could go through all that and explain each piece. But the normal answer of “spend a few hours reading the rust book, it will be faster then trying to muddle through” is fine, and misses the point.
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.
Not sure I follow. How did you ever read or learn anything more than pseudo code or, at best, Python? Any language at all will have hurdles like those.
Rust is a starkly different vocabulary and syntax than other languages I've learned. The error handling is different than both Python, C, Matlab, etc. Objects suddenly have ownership that has to be respected.
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.
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.
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.
> Rc is the non-reference counted version and Arc is the atomically reference counted version.
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.
love your code (erm, the product of your code, at least), use it every workday, literally
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)
I fail to see how this is any worse than a whole bunch of sophisticated Python, C++ or Java applications I've tried to browse. It isn't as simple as a single code unit with a main entrypoint directly in a `src` folder, but I don't expect such things from reasonably complex software projects.
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.
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.
I think your mental model is wrong if you thought a consuming operation like IntoIterator::into_iter is read only.
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.
There is a difference between a variable and a value. The value is definitely read only or you would need a mut modifier. The variable binding changes in a similar way to shadowing it with a new 'let' on the same name.
This would imply that the variable can get its value back once the new binding is out of scope. But that is not the case as this doesn't compile:
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.
> C programmers could read java code and immediately understand
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 *.
No. C existed long time before java, so the natural career progression was for C programmers to become java programmers.
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.
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.
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++.
“Const” doesn’t mean immutable in C++, in my book. It means unmodifiable or read-only. Immutable means the value won’t ever change, whereas unmodifiable means that the value can’t be modified via that type (but it might be modified some other way).
Constant is determined at compile time. It's constant over the every run of the same binary.
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).
An example of a constant would be a literal, such as "Hello, World!" or 4 or false
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.
But that's exactly the criticism: that C++ used the word const for something different from its theoretical meaning, something that could have been better expressed with the word immutable.
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.
Void is not a good example for you to point out. Rust's interpretation is correct: The unit type () is what most functions should return. The void type has no valid values at all, which means it cannot be returned. It is other languages that are wrong for using the term void. The actual void in Rust is called never: https://doc.rust-lang.org/std/primitive.never.html .
> The void type has no valid values at all, which means it cannot be returned.
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.)
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.
> 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.
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.
Agreed 100% The last time C++ "made sense" to me was 1998 reading and practicing Bjarne Strostrup's C++ book. It was better C then and a lot of these improvements made sense.
Scott Meyers was the go-to reference for "expert" C++ knowledge for over twenty years, he retired after his C++14 books came out and a mere two years later he couldn't even keep up with the language to make technical corrections.
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.
Heck, even C++ is getting out of hand for Herb Sutter. He might say "ISO C++ is the best tool in the world today to write the programs I want and need", but then he also says "My goal (with cppfront) is to explore whether there's a way we can evolve C++ itself to become 10x simpler, safer, and more toolable."
> Heck, even C++ is getting out of hand for Herb Sutter.
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.
"Effective Modern C++" had nothing on "Modern C++ Design", published in 2001. A coworker lovingly referred to it as "The Anarchists Cookbook of C++".
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.
> In C++11, there is no ergonomic way to do a lambda capture by move – which is usually how I want to capture variables into a closure
> 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.
Author of the blog post here. I am limited to C++11 for this, and will be limited to C++03 for some related projects. I did say C++11 in that comment, as I am of course aware that C++ has "fixed" that issue, but I'm still entitled to my gripes. I find it odd that someone claimed I was not aware the issue has been fixed in later versions, when I specifically said a version. People seem to be very motivated to assume I'm wrong about C++... because otherwise they'd have to admit that their favorite language has flaws.
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 :-)
"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.
I don't care if you do not like the lambda syntax (there is plenty of syntax of C++ that I dislike), but lambdas are now a pretty core feature of the language, so you better learn it or switch to another language.
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.
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.
Exactly, but using a different set of syntax, plus a new ruleset of capture semantics. The capture syntax especially is a minefield. And are the runtime guarantees exactly the same as that of a local class instance?
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.
>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
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.
> There is no good reason to use lambdas except parametrization of stl algorithms.
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.
I said "In C++11" because I am aware that it has been fixed later. But I'm stuck in C++11 for platform reasons and so I get to complain about its problems too. But of course, it is because I am aware that this has been fixed -- crappily -- that I qualified it. I'll add more qualifications for you :-)
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.
Well, at least with C++ you can write linked lists and other algoritms which require multiple references. Not every engineer is sloppy with use-after-free, incorrect mutability, leaking resources etc. There are correct programs written in C++, the developers are not unicorns and it does not require super human effort. Kitchen knives are also unsafe and dangerous yet with their correct use we can create wonderful meals.
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.
By that standard, how do you know correct programs are written in any language? I seem to recall you praising JOVIAL a week or two ago; well, how many full theorem-prover correct programs were written in JOVIAL? I would guess, zero.
Don't hold C++ programs up to a standard that programs in other languages also can't pass.
Well, I'll take your word on it. I was working from memory, not looking at a particular post. I apologize for the inaccurate claim.
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?
IMO not every coder is sloppy with use-after-free. Not everyone is slopy with incorrect mutability. [...] Not everyone is sloppy with $THING[N]. But it's hard to consistently not be sloppy in any one of them, and N are so many that almost everyone is sloppy with at least one of them, most of the time.
Each probablility of a mistake may be low on its own, but they compound with every avenue to make mistakes.
I've spent hours helping my students debug their Rust programs of incorrect array-indexing logic, incorrectly-reasoned loop invariants, incorrect conditional logic, and everything in between. They're not double-deleting raw pointers, but then again, most C++ programs don't use raw pointers these days either...
"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.
> Kitchen knives are also unsafe and dangerous yet with their correct use we can create wonderful meals.
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.
>There are so many features I miss from Rust, not only the obvious safety features, or even primarily those, but also features that C++ could easily add, like sum types (called enums in Rust), or first-class support for tuples.
Stopped reading here. std::variant and std::tuple.
> It can be done, but the calls to std::tie and std::make_tuple are long-winded and distracting
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.
I 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.
How do you represent state machines, ASTs, or messages of different types? Sum types are great for those! I assume you don’t use std::variant for that? Have you ever written a tagged union when std::variant was available for clarity? Why the tagging boilerplate?
I started writing C++ to do some JSON at work and it is absolutely horrible piece of shit language (even with nlohmann's and simdjson). I cannot believe people live like this.
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?"
You can do it using whatever you want. I hope I don't discourage you from discovering C++. You might enjoy it, you might not, who cares. Go do it and make up your own opinion.
Good. C++ is discouraging. It combines huge complexity with readily available footguns, and the prevailing attitude is that all developers must be able to write perfect code with a full understanding of the language. Otherwise any security holes or runtime crashes are their fault.
Oh! I've heard good stuff about Clion. I am pretty skeptical of vs code though, regarding comparing it's capabilities for C++ debugging and performance analysis. What plugins do you need nowadays. It's a good general purpose IDE for sure.
C++ is not for everybody. However it is one of the most successful programming languages in history. And there is a reason why. You might not personally be able to understand or appreciate why it is so successful of course. But that doesn’t change the fact that C++ runs the world.
> Unfortunately, I am all too well aware of why these decisions were made, and it is exactly one reason: Compatibility with legacy code. C++ has no editions system, no way to deprecate core language features.
Actually, 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... seems false? In particular, the point of an "edition" system is that code written for the new edition can seamlessly import code written for the old edition, and so a hypothetical pointerless "new edition" would simply disallow bare pointers in new files, and things like smart pointers would continue to be _implemented_ in old edition code.
> would simply disallow bare pointers in new files
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.
Have you looked at the CppFront[1] project by Herb Sutter? It's less of a completely new language like Carbon. It's meant to be C++ with a "nicer" syntax, while providing 100% linking compatibility, and with 100% source backward compatibility available when desired.
You can get to a pretty clean modern C++ by convention and compiler warnings and static analysis.
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.
C++ was grungy in the 90’s, and, well, Rust as it is now is better than C++ as it is now, the comparison that matters now. Rust in 30 years will be better than C++ in 30 years. And safety is a big deal feature, and Rust has it. If it doesn’t have future big deal features, future languages can have it.
I reject the notion that C++‘s “maturity” is a substantial advantage given its unsafety.
I just don't use out parameters and avoid bare references altogether, const lvalue and rvalue references express idiomatically whether a parameter is to be copied or moved. Structured binding makes it trivial to consume returned tuples, so I don't find this a big issue.
You still have to write boilerplate to return them, though :-( But I agree it's better :-) Unfortunately, I'm on C++11 for this project, will be on C++03 for others :-(
For me, it's the fact that there is no standard package manager for the language. Finding the right linker/include args is soooo not user-friendly. Also, these `-I` and ˋ-l` flags can differ across platforms (and so can the installation procedure for these libraries). Yesterday, I tried to run an old project of mine developed on linux, but this time on a mac. First, the linker complained it could not find -lglm, I had to search a bit to find out that this is gl math. Then I searched this in homebrew, I also looked at different package managers. The low quantity of packages available surprised me. The most interesting package manager looked like it required python. Can't the community build a package manager in their own language? Anyway, at this point I gave up for the day.
I love the language overall. Seems like standard & easier management of dependencies and compiler flags would help it thrive again.
You can't have an effective package manager for a compiled language unless the package manager is actually a part of your own build system.
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.
As the name suggests, this wraps the whatever build system that the dependency is using rather than replacing it with Meson. Say if you depend on Boost and Qt, you will end up using both Boost Build and CMake in addition to Meson (most likely there will be a couple of more build systems if you are also building Boost's and Qt's own dependencies such as ICU). This has two major drawbacks:
1. 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
Tbqh after figuring out make + pkg-config, it's kind of zen. C/C++'s unique dependence on the system's package manager for libraries is weird, but I can't say I hate it.
> There are so many features I miss from Rust ... like sum types (called enums in Rust)
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.
Something const would catch is rarely the cause of broken software. In my experience, most issues in software are logical and often derived from complex behaviors and interactions, not misuse of syntax.
> 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.
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.
> 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.
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.
Every language, every rule, every suggestion has exceptions and gotchas. That's just life, and programming in general.
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.
const never really worked with other C++ features[0], so in any big pile of code you're better off not using const.
[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.
No, you cannot use '= default' for the assignment operators if you have custom copy/move constructors. I thought this was clear that this was my complaint in the post. I will make it clearer in the post. IF you customize the constructors, THEN =default will break your code, even though the correct implementation is entirely boilerplate.
And for tuples, you still need to use std::tie or std::make_tuple on the other end.
All valid points. I agree that C++ as a whole is insanely big and complex and I can't even dream nor that I want to know about every nook and cranny.
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).
Exactly. Just use what you need. Only use the big hammers when absolutely needed. That's the power of C++ IMO. Lots of big hammers out there, but lots of smaller ones for when you don't!
I agree with the blog post, also having learned C++ followed by Rust. Only in the context of saner modern languages like Rust did I see C++'s footguns in retrospect.
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.
> Side note: Someone brought up structured bindings in a comment, as if this fixed the issue. Structured bindings are a great example of the half-way fixes that proponents of modern C++ love to cite. Structured bindings help some, but if you think they make returning by tuple ergonomic, you’re mistaken. You still need to write std::pair or std::make_tuple in the function return statement.
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;
}
I've used C++ from its inception (when it was cfront C++ -> C translator) to modern C++, not to mention many other languages over the years. Totally agree with the author that C++ manages to accumulate many bad decisions over time, leaving a legacy of overly complex or under-powered features.
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.
Another love letter to Rust is always welcome. I'm extremely pleased to see another compiled language that makes developers so excited to write code. I do think that it's entirely possible, though, that the warts that Rust does have will start to surface in time. There are no silver bullets.
280 comments
[ 2.7 ms ] story [ 253 ms ] threadReference 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.
(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.
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.
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.
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.
[1]: https://www.aristeia.com/books.html
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.
[1] https://en.wikipedia.org/wiki/Stockholm_syndrome
"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).
Well. I can read that! Next line... ...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.
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.
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.
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
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.
[1]: https://sr.ht/~pierrenn/ripgrep/
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:
This doesn't work: 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.
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.
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...
https://github.com/hsutter/cppfront
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.
> 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.
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.
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:
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.
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++.
[1] https://github.com/gfx-rs/wgpu/
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.
Actually, 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.)
[1] https://github.com/hsutter/cppfront
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:
https://isocpp.github.io/CppCoreGuidelines/
I reject the notion that C++‘s “maturity” is a substantial advantage given its unsafety.
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.
https://mesonbuild.com/
The dependency declaration and auto-detection is nice. But the hidden extra is WrapDB, built-in package management (if wanted):
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
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.
* 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).
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.
No you don't. A tuple needs to be declared once. Then everything else is sugared.
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.
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.