We used C++20 to eliminate a class of runtime bugs
devblogs.microsoft.com
devblogs.microsoft.com
Enjoy a sample of AOSP source code, https://android.googlesource.com/platform/art/+/refs/heads/m...
You posted this for a reason .. would be good to get your thoughts.
edit: adding one more point. I went to a fairly high-ranked university(top-20). We would get graded based on how clear our code was, comments (both for functions and inline comments). This code would get a zero on those metrics.
https://android.googlesource.com/platform/art/+/refs/heads/m...
like, just this, basically C:
if (strncmp(argv[arg_idx], "-XXlib:", strlen("-XXlib:")) == 0) {
...
is just so complicated and error-prone over if(std::string_view{argv[arg_idx]}.starts_with("-XXlib:"))
...
(which I guess is the intent)The code it replaces also had a strlen in it.
(not OP). I examine the output if someone tells me it's slower, or if I care about performance. I did a very rough benchmark which says in this case they're equivalent [0], however that's not necessarily representative of the places it's called in. There's definitely a compile time overhead though [1]. On my current project there's ~20k files. On an 8 core machine (as an example), adding 150ms per file would add 7 minutes onto a clean build (before optimising the builds).
[0] https://quick-bench.com/q/LBarCDIigwpmXcwZSx-RgV2x4_4 [1] https://build-bench.com/b/OH8qf9AgEB_l-BhANV5qP_uSCys
So you either validate it actually takes place or believe in fairy tales.
So, the example is of bad old code, at a place that enforces bad coding practice. The intent appears to be to suggest this is typical practice, which is not supported.
If this what a company with deep roots in C++ world is doing, what are the large majority of "dark matter" software factories doing, specially the sweetshop ones.
There are shops where people still write new C++ like it's 2011, or 2003, or 1998, or 1992, or C. There are plenty of shops where people code the best way their current production compiler allows. There are plenty where different people do some of each of those. Vanishingly few shops make an effort to rewrite ancient code according to current best practice.
I guess a sweetshop company is one with Oompa-loompas.
I guess I need to catch up with some Twilight Zone episodes.
Looks just like regular PHP. So what's the point? Or did I miss something?
/s
In fact, if we focus on traditional enterprise shops they are stuck in what I call the C+ mindset.
Those of us that bother to live in HN, Reddit, attend CppCon, C++Now, C++ on Sea, ACCU in whatever form, are 3lit3. A tiny minority that actually cares about quality.
That is the point.
In fact, see Python 2 vs 3 for probably the most impactful case of people being slow to take up new language versions/features.
There's going to be shops writing C89. Some reasons might be well founded, some reasons might be less well founded.
Lasted surveys, which are anyway only answered by people that are active enough to care about stuff outside job, places use of tooling to validate modern practices at about 20%.
Now do some statistical extrapolation and it is relatively easy to find out how much "modern C++" is actually used in practice.
Bjarne hasn't dedicated two CppCon to the subject to improve adoption of better C++ practices just by accident.
So in result, just by the means of statistics, all code is necessarily crap. (Besides the extremely seldom pearls written by some independent geniuses).
Frankly this is unavoidable on an industrial scale of production, I guess. At least as long as we can't clone "10 x programmers".
What am I looking for? Is it just the use of smart pointers?
Personally I wouldn't like to slog through this on a moment's notice but it seems named well and there are a lot of comments, and appears to have been through clang-tidy.
That's why repairing design mistakes is only achievable by rewrites, inevitably breaking compatibility with the old system in this process.
For instance, I've been bitten recently by the new "universal" syntax to write variable definitions with only curly braces.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
// Forty-two things:
vector<string> v1 { 42, "foo" };
vector<int> v2 { 42, 1 };
vector<char> v3 { 42, 'x' };
cout << v1.size() << " " << v2.size() << " " << v3.size() << endl;
return 0;
}
This prints, of course:> 42 2 2
Because this is actually still a shift operator, it retains all the precedence of the shift operator even when tired humans think of it as as a clever new stream out operator. So if you're in a situation where shift has precedence, the operator fires, and too bad if that wasn't what you'd intended nor what makes sense to a human reading the code.
Oops.
Some languages let you mint new operators (from some limited combinations of symbols) or allow infix function calls, or by some other means provide for you to nicely extend syntax here, but the overload of unrelated operators was IMNSHO an ongoing disaster whose popularity with programmers is unexplained, like C's decision to treat an array as just the pointer to the front of the array.
But I'm a cranky old man, I also don't like the fact that Rust's + concatenates Strings (this isn't inherent in the language since String isn't a built-in type, in Linux kernel Rust nothing like this exists, because Linus would throw a fit, but in the out-of-box runtime environment Strings implement the Add operator as a concatenate operation).
I'm firmly of the opinion that if anyone uses a sane programming language first, they'll absolutely do anything they can to avoid C++.
Corollary being, 1) if C++ is your first language, you'll end up learning all these rules and exceptions to the rules and assume that is just how it is (this is the way I think about Python), and 2) if you use C++ you are aiming for performance that alternative languages cannot provide. I'm glad the gap between alternatives and C++ is narrowing. If I could, I would never write another line of C++ again. Rust and Zig are FAAAARRRR better alternatives to me personally, purely because there's fewer gotchas I have to keep in my head when attempting to do anything with those languages.
One of the gripe I have with C++ is that it has replaced C as the goto language for general purpose libraries, yet it's hard to built bindings for another language (whereas bindings to C are trivial).
It feels impossible to use Rust or Zig, but I have hope I can use Nim. It can compile to C++ and has a simple-ish way of wrapping C++ code. Maybe it would interesting for you too?
I'm on a codebase of a few hundred kloc and the need for a vector initialized to anything but the default value pretty much never came up. It uses boost, Qt, llvm and two dozen other libraries, yet there is a grand total of eight instantiations of that constructor in it.
When putting a breakpoint, it turns out that all the calls to it I could trigger actually just initialize with the default value statically (e.g. vector<T*> v{x, nullptr};).
Three instances I could grep immediately, all from the same file:
std::vector<std::tuple<std::optional<qreal>, QString, QColor>>
values( // Not list-init
numValues, std::make_tuple(std::nullopt, QString(), QColor()));
(...)
std::vector<size_t> nextTupleIdx(numValues, 0); // Not list-init here!
std::vector<bool> done(numValues, false); // Not list-init here!
I do not doubt a single second that there are 36 ways to write this that are simpler, more efficient and more readable. Yet I would like to hear your suggestion. std::vector<T> vec(numValues);
as vector value-initializes.Personally I prefer an explicit reserve or resize, but still..
The {42,1} is an "initializer list", which was a way of passing a list of things to a constructor.
However, at the same time, something called "uniformed initalization" was added, which used {}, to "clean up" how constructors work.
However, from looking at a piece of code it's impossible to know if it is trying to call "initializer list" or "uniformed inialization". The "initalizer list" wins in this case, when the items are all things you can put in the vector.
It makes code look silly, too, as now initialization looks different from assignment.
int i { a + 2 };
i = a + 4;
int j = a + 2;
j = a + 4; // ah! consistency!The first one will call the initialization constructor, whereas the second form will initialize the object and then apply one form of copy construction, depending on which ones are available (user provided or compiler generated).
It just happens that for int that doesn't matter in practice.
In a language that isn't just pretending to be strongly typed, the compiler would notice that char and whatever-integer-type-42-is aren't the same type, and it would reject your program as nonsense.
But that confusion gets much less chance if the language explicitly distinguishes initialising collections so that "I want N of X" is different from "I want N, and X". Compare Rust's vec! macro:
let mut v = vec!['x'; 42]; // NOTE semi-colon. Compiles, vector of 42 x characters
let mut v = vec!['x', 42]; // NOTE comma. Does not compile
let mut v = vec![b'x', 42]; // NOTE comma. Compiles, vector containing 120 (the ASCII code for 'x') and 42 as bytes
let mut v = vec![b'x'; 420]; // NOTE semi-colon. Compiles, vector of 420 bytes with value 120
let mut v = vec![b'x', 420]; // NOTE comma. Does not compile unless you explicitly tell Rust that you mean the low 8-bits of 420 if you do this
I'd actually be enthusiastic about a Clippy warning for the middle one, because it feels like the odds are better than they should be that wasn't what you meant. But on the other hand, the odds of you wanting either possibility are slim, so, not a priority.But what they do with semicolons is pure brain damage. Having different complex semantics encoded purely in semicolons is nuts imho. I don't get how someone could create such mess in an otherwise mostly quite sane language.
Especially as semicolons are an archaic artifact that shouldn't be used for mostly anything in a modern language, imho, as it's just useless noise (besides when writing some well readable one-liners of course ;-)).
Different people wanted "{ } for unified construction" and "{ } for initalizer lists", and we ended up with this mess which made 0 people happy, and ruined unified construction.
Stuff like this terrifies me that I'm signing up for some kafka-esque frustration.
Is there a way out of this (serious question)?
string s; // initialized (empty)
int i; // NOT initialized (garbage value)
static int i; // initialized (to zero)
char c; // NOT initialized (garbage value)
...I mean, it’s always been a complete mess.What's valuable is being able to compile format strings into faster code, like the formatter macro in Common Lisp.
So balance that. If you're not just distributing binaries, if you have geek end users compiling, maybe don't always use the latest bleeding edge C++?? features.
Meanwhile I am enjoying modules in VS 2022, even if there are some hiccups.
(FYI: for anyone holding off on this because of issues with spdlog, enough has been fixed in the latest versions of both that you can upgrade them together now.)
Also worth noting: the UX of fmtlib's compile-time format strings is actually quite interesting. There are error callbacks in the consteval calls that take advantage of the context that compilers show in compilation failures that detail the mistake being made.
(Aside: I'd also like to note that `absl::StrFormat` has supported compile-time format strings for quite a long time. https://abseil.io/docs/cpp/guides/format)
It doesn't seem incredibly valuable to know that some error() calls in untested code are well-formed, since that code could be broken. The error() could be a false positive, or the compiler could crash before reaching the error() call due to some bug. Or some of the errors() could be in de facto unreachable code: no test case can cause them to be executed.
If you have a test suite which hits all the error() calls, and if the formatting system is robust to catch bad arguments at run-time, you don't have a problem.
But from the little I've learned about modern C++ and RAII, I wonder how systems engineers will respond to Rust in the long-term. Memory safety seems to be the primary argument in favor of using Rust, but what would be easier - rewriting everything in Rust, or refactoring existing code to take advantage of RAII?
RAII is the one thing I really really miss from my C++ days. From what little I know about Rust it seems to promise a more "baked-in" version of RAII so I'm all for it. Haven't found the time to experiment but looking forward to eventually dipping my feet in.
For Rust, it's the "Drop" trait that you add onto things which ultimately causes RAII behavior.
You don't need to do it as much with rust as a lot of the reasons for wanting RAII (memory management) are simply handled by the language.
For Rust, it's sort of the "Pit of success" the easy thing to do is the right thing to do. Whereas in C++, it's pretty dang easy to new something up and fail to correctly delete it.
This is true, but we shouldn't be encouraging people to reach for new/delete these days; auto Foo = std::make_unique<Foo>(args); does the right thing in a surprisngly large number of cases. It's not the only tool available, but it's a really damn good one.
I'd argue that in a language with the `new` keyword, nothing is more easy or natural than invoking it. `auto Foo = std::make_unique<Foo>(args);` is absolutely the right thing to do, but would you really be surprised to find `auto foo = new Foo(args);`?
I absolutely agree unique pointers are the right call. They just aren't necessarily the easy or natural call.
The contrast is rust where all ptrs are, by default, unique ptr and the compiler validates that for you.
It is already hard to educate people on modern language features and adoption of static analysers that enforce Core Guidelines.
Who would write the books and teaching materials for #pragma modern_defaults, and ensure it would be accepted into ISO?
Foo foo{args};
Which is simpler than the other cases, more correct and more performant. The rare few times dynamic allocation of something that is not an array with new is needed is when you have polymorphic types - but then, the user code generally calls a factory function instead of new. auto foo = std::make_unique<Foo>(args);
Think about explaining this to a new C++ developer.This will feel much more natural to most developers:
var foo = new Foo(args);How's this:
The code allocates and initializes a new Foo instance with args. By using make_unique you guarantee that the destructor is called when foo goes out of scope for any reason.
Foo foo(args);
Sometimes an object has to be on the heap as its own separate allocation (rather than on the stack or as a data member), but I notice that some new developers who come to C++ from some other language use the heap way too much.And with help of Roslyn, any type with a "destructor" (aka Dispose) that gets used without a using declaration can be turned into a compiler error.
Oh, refactoring existing C++ code is easier, hands down.
You can, for the most part, keep your C++ algorithms exactly as is with little rewriting. Rewriting in rust may force you to totally rethink your approach (or use a lot of unsafe, or write really inefficient code).
to answer the more general question, I think most engineers would prefer to refactor a c++ project over rewriting the whole thing in rust, and rightly so. you usually don't want to fuck around with delicate things that currently work in these sorts of projects. perhaps over time you end up with an FFI layer over a thoroughly tested c++ core. then new features can be written in rust or whatever friendly language is popular at the time.
Putting it that way, I can see why someone would appreciate that Rust guarantees safety by default, instead requiring you to opt-out explicitly when needed (with `unsafe`), versus the opt-in nature of RAII - for greenfield projects, anyway.
The built in smart pointers take care of much of memory management. (Although they use RAII internally)
Personally, I maintain quite a lot of open source C++ code -- I'm not rewriting that in Rust, but I'm writing new projects in Rust.
With everything integrated like, you know (and this may sound super crazy) graphically-mouse-operated "create new project", "right click -> add new file to project", resource editor with "create a window with a button, double-click the button and generate a function to handle the event"-sort of thing. F7 to build, F5 to debug. More or less the things we are used to since the past 30 years or so!!!
I got help to implement it, it was a bit difficult, but quite nice to have.
What's it like these days? Is tooling pretty much the same - downloading packages from linux, some header only libs, and makefiles? Has the Language Server Protocol made in roads with C++ editors and IDEs?
Editors all know C++ now. Generic lambdas in C++14 made coding fun. Template metaprogramming is hardly ever needed or useful anymore. Builds got faster with module support (essentially standardized precompiled headers), and also ccache and ninja. As Concepts penetrate, error messages get radically better.
Valgrind is increasingly unnecessary, as use of op new vanishes.
For example, annotating a parameter with 'auto' instead of the typename T preamble.