C++23: The Next C++ Standard
modernescpp.com
modernescpp.com
#include <print>
int main()
{
std::print("hello, world!\n");
return 0;
}
So, it has come to this. Much better than overloading bit-shift operators for input/output. int main() {
printf("hello, world!\n");
return 0;
}
C knew it all along :)<format> is instead 100% typesafe. If you pass the wrong stuff it won't compile, and as {fmt} shows you can even validate formats at compile time using just `constexpr` and no compiler support.
It's hard to take people seriously that try to talk it down for being a pragmatic solution that's been around for probably 30-40 years.
I'm often working with fixed size types, and still find myself using %d and %ld instead of PRIu32 etc most of the time, because it's simply easier to type. If unsure about portability issues it can help to cast the argument instead. But realistically it isn't much of an issue, printfs seem to be about 0.5% of my code, and >> 90% of them are purely diagnostic. I don't even recall the last time I had an issue there. I remember the compiler warning me a few times about mismatching integer sizes though.
A common use case for cstdio/iostreams/std::format is logging. It's not at all uncommon to have many, many log statements that are rarely executed because they're mainly for debugging and therefore disabled most of the time. There you go, lots of rarely used and even more rarely 'tested' formatting constructs.
I don't want things to start blowing up just because I enabled some logging, so I'm going to do what I can to let the compiler find as many problems as possible at compile time.
void msg(char *msg, char *x, int k) {
printf("%hhn%s%d\n", x, msg, k);
}
So, how about it? I mean, I have code where that works exactly as expected, so I can "know it works" according to you, but I also have code where that blows up immediately, because it's complete nonsense. Which according to you shouldn't happen, but there it is.Another case in C++ is if the value is templated. You don't always get test coverage for all the types, and a compile error is nice if the thing can't be handled.
"Type coverage" is pretty useful. Not a huge deal here I agree, but nice nonetheless.
If the compiler can statically check that your format string is valid and type checks, you can prevent this entire class of exploits.
Not sure what you're trying to say here.
Some compilers can keep format strings type-safe, but they are going above and beyond the standard to make it happen.
That's a very low bar though.
There are widely used languages that don't have a standard at all; literally every single thing you can do in them is above and beyond any sort of standard.
Edit: OK, I guess puts includes a newline, so you'd need to use fputs if you don't want that (although this example includes one). Still, both of those are much less complicated than printf!
To be fair to C++, this is undefined behaviour in C until C23. Prior to that the () is interpreted as (...), varargs, and not (void).
So, some mutually beneficial cross pollination of ideas.
Overloading gives user defined types the expressiveness of internal types. Like all features, of they are used badly (e.g when + is overloaded to an operation which can hardly be interpreted as addition) it makes things worse. But you can write bad code in any language, using any methodology.
The moment you have a language actually designed for numerics all these things vanish. One of Julias core design aspects is multiple dispatch, including operator overloading and it works extremely well there.
I also don't see the point for discoverability at all. The documentation will list the overloads and the non-overloaded calls are exactly as discoverable as the others.
Function names let you clarify that it is an outside product or inside product (e.g. there are often different types of adds, multiplies, divides), and I can not stand when someone maps cross product onto ^ (because you can both exponent and cross product some vectors, like quaternions, so why use exponent operator for cross?) or dot product onto something else that doesn't make any sense. Also operator overloading often doesn't make clear memory management, rather it relies on making new objects constantly, whereas with explicit functions, you can pass in an additional parameter that will take the result. Lastly, explicit functions allow you to pass additional information like how to handle various conditions, like non-invertible, divide by zeros, etc.
I find word-based functions more verbose but significant less error prone and also they are more performant (because of the full control over new object creation.) Operator overloading is only good for very simple code and even then people always push it too far so that I cannot understand it.
[1] 1997: https://github.com/bhouston/BezierCurveDemo1997/blob/master/..., 2001: C# math library: https://github.com/bhouston/ExoEngine3D/tree/master/Librarie..., 2013: a bunch of the core Three.js math library, 2023: https://github.com/bhouston/threeify/tree/master/packages/ma...
We have move semantics since C++11.
Alternatively, the main reason to use operators here is infix notation, so perhaps Haskell-like backticks.
plus(
mul(vec1, vec2),
mul(vec3, vec4))
Yeah it's a tiny bit clumsier, and prefix notation takes some getting used to. But on the plus side we avoid all the too-clever travesties programmers have inflicted on us with bad operator overloading decisions! On the whole I think it's easily worth the trade. (plus
(mul vec1 vec2)
(mul vec3 vec4))
Lisp is love.As an example, early device models for circuit simulation were not designed to be numerically differentiable, leading to serious numerical artifacts and performance issues. Now we have courses dedicated to designing such models, and numerical analysis is used and emphasized throughout.
Is there anything today that you look at and think "yeah, they're gonna need to fix that at some point"?
> And that's speaking as someone who once implemented a path class with a subtraction operator that would return the relative path between two absolute ones. I thought I was very clever.
Haha! It's ok. The temptation to be clever with operators is too strong, few can resist before getting burned (or more usually, burning others!) at least once.
Perhaps that idea falls apart once you realize you would need hundreds of symbols for just addition…
But what if those symbols were (automatically) imported and named at the beginning?
Perhaps it would be annoyingly inconsistent how in various files different symbols are used for the same operation…
I realize this is a minority opinion, but I don't want my editor to replace anything unless I've deliberately told it to.
sum({ mul(vec1, vec2), mul(vec3, vec4) })
definitely is.That said, in my experience over the decades, operator overloading has been one of the primary causes of bugs that are very hard to pin down, so I have come to hate it. It hides far too much.
The cost/benefit ratio of operator overloading is generally unfavorable in practice, in my experience. Which is not to say it shouldn't be used when it actually clarifies things! But those situations tend to be fairly niche.
Interestingly, where I work right now, using operator overloading is specifically prohibited. So I'm confident that my dislike of the practice is not just a personal quirk.
In other places they monkey patch c++ defincies as a language.
And they are confusing and error prone.
Nobody is pretending we will get rid of any c++ syntax ever. So the discussion is about a hypothetical language syntax that fits C++ slot.
In that world C++ would have N x M matrices as native value types in the language (as fortran does) and those operators would be defined in the language spec for matrix types just as they are defined for standard number types at the moment.
And it would not have any operator overloading.
So, no overloading for decimal types? Nor fraction types?
I may be the oddball here, but I've used operator overloading on those types more than I have for vectors and arrays.
That may be true for C++ (I'll take your word for it), but not for all programming languages in general. For example, in C# it's fairly common to overload == and != to implement value equality for reference types (classes).
Of course, you should really only do this for immutable classes that are mostly just records of plain old data. And C# 9 introduced record classes, which is a more convenient way of defining such classes. But record classes still overload these operators themselves, so you don't have to do it manually.
OK.
Operator overloading is a useful feature that saves a bunch of time and makes code way more readable.
You can quibble whether operator<<() is a good idea on streams and perhaps C++ takes the concept too far with operator,() but the basic idea makes a lot of sense.
string("hello ") + string("world");
complexNumber2 * complexNumber2;
for (int i : std::views::iota(0, 6)
| std::views::filter(even)
| std::views::transform(square))
someSmartPtr->methodOnWrapperClass();"saves a bunch of time and makes code way more readable"
Not when everybody defines their own operators.
Note - we are discussing operator overloading, not operators as features in syntax. Operators at the syntax level make life a lot easier. But then everybody uses the exactly same operator semantics, not some weird per-project abstraction.
The lines of code you wrote as an example are not saving anyones time, except when writing it if you are a slow typist and lack a proper IDE support for C++. If typing speed is an issue, get a better IDE, don't write terser code.
I think your argument is basically "people should not aggressively violate the implicit bonds of interfaces", which is true. But that goes for all interfaces, not just and not in particular those around operators.
We just have cases where it's common with operators because those are one of the few cases where we have lots of things that meet the interface and interact directly as opposed to hierarchically. The same kind of issue comes up with co/contravariant types and containers sometimes, but that's less often visible to end developers.
The lines of code in the example save reader's time as they focus attention on the actual business logic.
Yes, this assumes that operator overloading follows some convention, but you need to follow conventions regardless to make readable code.
I think what people should advocate is full DSL capabilities with some unambiguous gate syntax so people know precisely that `foo * bar` is not using the host language syntax. Overloading operators is ambiguous and vastly incomplete (everyone is holding up matrix math as the shining example for the utility of operator overloading and you can't even express dot product notation in C++!)--it's a hack at best.
Except now you replaced + with a name that tells you just as much/little as + does. So you made your program verbose for the sake of verbosity.
string("hello ").append("world");
complexNumber2.mult(complexNumber2);
// wtf is even going on with this one in your example? have these people never heard of method chaining?
for(int i : std::views::iota(0,6).filter(even).transform(square))
(*smartPtr)->methodOnWrapperClass();
That's all about the same verbosity, it's much more clear to the reader even if they're unfamiliar with your codebase, and dropping operator overloading eliminates the "clever" option to do stupid crap like divide file path objects together. 3.times(2).plus(7)
Some things just lend themselves to being expressed in terms of simple operators. (*smartPtr)->methodOnWrapperClass();
That is still using the overloaded SmartPtr<>::operator*() method.I understand the viewpoint that operator overload is syntactic sugar for things that can easily be done another way, I just disagree that costs outweigh the benefits.
Overloading operator| is a crude way to get the equivalent of extension methods without having to change the language.
The iostream bitshift overload was one of the first features of C++ that I learned to despise. I'm very happy that there's an alternative in the new version.
For my part, I've been persuaded that generic operators like that are a net win for math-heavy code, especially vector and matrix math. Sure, C++ goes too far, but there are middle grounds that don't.
For numeric value types mathematical operators are the only sane option.
For arbitrary classes - not so much.
A sane language in the slot of C++ in the language ecosystem would not have operator overloading. It would have matrix types defined in the language spec with mathematical operators operating on them.
Some folks prefer absl's flat_hash_map over std::unordered_map for a hash table, and it's not great that you need to choose or risk having both in a codebase, but it _is_ nice that you can have your preferred hash table and use operator[] whichever you decide.
Python also has operator overloading, and people seem to like that numpy can exist using it. And container types. Weirdly doesn't cause much consternation compared to C++ (maybe because the criticisms of the latter come from C programmers?)
I've occasionally missed overloading in JS/TS though.
This is unfortunately impossible (IMO). The problem is matrixes have multiple operations that don't translate nicely like complex numbers do. If you want to be consistent, you have to pick and choose What A * B means, under which contexts, and when is that illegal (or what should happen on an error).
For complex numbers, there's only one definition of A * B that matters and no failure cases.
I fear there's not clean way to do matrix operations that won't make some community really irritated for choosing "wrong". (Physics, engineering, science, etc.)
MyVecModule(v1 * v2 + v3)
Easy to read and explicit enough to know which functions is used.
https://dev.realworldocaml.org/files-modules-and-programs.ht...
The modern web is built on overloading the . operator (e.g. ORMs like Rails and Django). We will never see a Tier-1 ORM in Golang simply because it lacks it.
Gamedev, AI also benefits heavily from it.
Not true. It can be replaced with codegen.
See Ent (spun out of Facebook I believe)
Any advantages of Go (and there are many) are outweighed by the fact that you have to write and read 2x more code to be equally productive as Rails or Django.
That sounds like a good thing, having dealt with Hibernate in production. As a backend developer, I'm pretty happy with C++17 (and beyond), Go and Rust. All of them can be used in fairly explicit ways, which means debugging a problem is easy, and performance issues are right there on the page if any. I want less magic, not more.
Magic is magic until it becomes understood, then it is science.
While I don't want junior programmers wielding the dark magic of operator overloading, I trust that the engineers behind Django are using it reasonably.
Operators like +, -, /, *, etc have meanings independent of integers and floats and to not allow these meanings to be expressed is sad.
I've heard many programmers express this sentiment and what they actually are attempting to argue is that having overloads of these operators that do not respect the corresponding group, ring, or field laws is confusing. This I agree with. Operators should mainly follow the proper abstract semantics.
Allowing ANY operator to be overloaded was dumb, like C++ did, where you could do batshit crazy stuff like overloading unary & (addressof) or the comma operator (!), or stuff like the assignment operator (that actually opens a parenthesis about how copy/move semantics in C++ are a total hack that completely goes OT compared to this).
Sensible operator overloading makes a lot of sense, especially when combined with traits that clearly define what operations are supported and disallow arbitrary code to define new operators on existing types. Rust does precisely that, and IMHO it works great and provides a much nicer experience than Java's verbose mess of method chaining.
The Google C++ style guide has a very nice overview. There are only two pros listed, and large number of cons. And this document is old by Internet (dog) years -- at least 10 years.
Ref: https://google.github.io/styleguide/cppguide.html#Operator_O...
Built in operators don't always map 1-1 to CPU instructions so don't appeal to that authority. There are still plenty of CPUs -- old and new -- without multiplication, division, or floating point support.
vec3 foo{1, 2, 3};
vec3 bar{2, 0, -2};
auto baz = foo + bar; // {3, 2, 1}
[1]: https://github.com/g-truc/glmhttps://github.com/g-truc/glmConsidering there are like 3 different types of matrix multiplication operations, I don't think it's obvious at all. Feels like you should either use a language with complete support for implementing custom DSLs (that can express the whole domain naturally) or eschew ambiguous operator overloading altogether (gaining consistency and quality at the expense of a few keystrokes).
DSLs just push the complexity away from the language into someone else’s problem in a way that has much higher sum complexity. You’re making authors of numerical libraries second-class citizens by doing so. For some languages that’s probably not a bad choice (Go is one example where I don’t feel the language is targeted at such use cases).
Also, the lack of a standard interface for things like addition, multiplication, etc. means that mathematical code becomes less composable between different libraries. Unless everyone standardizes on the same DSL, but I find this an unlikely proposition, given that DSLs are far more opinionated than mere operator overloads.
Yes. And it tortures me every time.
I religiously avoid string concatenation in Python for this very reason. It's not that "+" necessarily means addition; it's that it always means a commutative operation (to somebody who has learned some algebra). String concatenation is notoriously non-commutative, thus it is extremely disturbing to write it using a visibly commutative operator. Any other operator except "+" would be better. For example, a space, or a product, or a hyphen. Whatever. But please, not a commutative operator. It breaks my brain parser.
When it comes to languages that let you mix strings and numbers, Lua has it right. + always adds, and accepts numbers and strings that can cleanly convert to numbers. .. always concatenates, and accepts numbers and strings.
I love me some operator overloading. I love / for filesystem separators, I love | for piping things. I don't like << and >> so much but that's just because of too many years of writing them everywhere.
1. Operator overloading
2. Operator desugaring (e.g. __subscript__(), which substitutes the intrinsic function for basic types, but can also be defined for user defined types)
3. Writing templates with weird adaptors for primitive types.
Given that its design goal was to embed C, there were already operators that worked with various and mixed types. Adding (+.), etc., would have been unacceptable to the users. So, I think in general, for this language, it was good but, unfortunately, iostream made people think you should overload the behavioral expectation, too.
Bjarne has given a couple of interviews on the matter.
If you wanted to use an abstraction over non primitive types for such things you would use a normal function.
[1] https://en.cppreference.com/w/cpp/io/cout
The others are more sus but you can make a bad API out of anything.
Rewriting (say) a load of calculations as a tree of sum pow exp etc. is just a huge burden - a codebase I work on has a formula that takes up about 6 lines for example, outputted by mathematica: total pain to to translate to function calls.
I quite like that one but I'd be willing to give it up for the hobgoblin of my little mind.
Most programming languages use infix notation for mathematical operations but polish notation for function calls. This creates an inconsistency. In languages, like LISP, that entirely use polish notation the inconsistency does not exist.
One could argue that if a programming language has this inconsistency, then one should at least try to be consistent with one's notation, i.e. for mathematical operations use infix notation (operator overloading).
Like with many things in C++, its another grenade, but when used appropriately is pretty great
In Scala, infix operators and methods are the same thing.
a + b
a.+(b)
Or a.map(b)
a map b
It's never been clear to me why there should be special "operator" things.https://github.com/manifold-systems/manifold/tree/master/man...
Lunch, behind the cafeteria. Closed fists allowed, nothing below the belt.
OK, come to my glass labyrinth where you don't know which of a dozen images of me are the real instance.
You will soon appreciate obfuscation techniques.
https://brevzin.github.io/c++/2023/01/02/rust-cpp-format/
I think Barry under-estimates how long it will be before C++ programmers actually get the equivalent of #[derive(Debug)] and as a result the impact on ergonomics. But of course I might be wrong.
main()
{
puts("Hello, world");
}acktually, you should use println.
class cppstream:
def __lshift__(self, right):
print(right, end='')
return self
cout = cppstream()
endl = "\n"
cout << "This" << " is" << " cursed" << "!" << endlOn DuckDuckGo, the very first result for "c++ string formatting" is the exact thing this person was complaining about.
- template-function parameters (NTTPs with a function parameter syntax rather than a template parameter syntax, tentatively spelled as `void foo(template int constant){}` )
- Scalable reflection, in combination with expansion statements (likely in C++26, spelled `template for (auto& e : array) {}` ) which would allow you to write an arbitrary parser for the template string into real C++ code. Reflection targets C++29.
Syntax2 already supports string interpolation as a special compiler feature.
C++20 added std::format [1], similar to Python's str.format [2] (~2010).
But OP said f-strings [3] (2016) which is string interpolation, a language-level feature.
C++ does not have string interpolation.
Not massively important, but IMO quite handy for inline code, e.g. SQL parameterization.
[1] https://en.cppreference.com/w/cpp/utility/format/format
[2] https://docs.python.org/3/library/stdtypes.html#str.format
[3] https://docs.python.org/3/reference/lexical_analysis.html#f-...
...Or phrased another way, they finally added them in the latest spec (which the majority of C++ devs aren't even using yet)
There's also a format-like thing using integers surrounded by curly braces, but it's horrific and I'm afraid to copy-paste it here.
Of all the things C++ annoys me with, I don't care that much about syntax.
First, includes either need to be wrapped in angle brackets (for files included from the include path passed to the compiler) or quotes (for paths relative to the current file).
Second, the whole standard library would be huge to pull in, so it is split into many headers, even for symbols in the top level of the std namespace.
Any decent language with modules needs no make utility in the first place. You tell the compiler to build/update the program. The compiler compiles (if necessary) the interface definitions the program references, and in that vein recursively updates the whole tree, compiling anything that has changed; then links the program.
I didn't need any make utility when working with Modula-2 in 1990!
There are two ways you might want to have this work anyway despite namespacing. One option would be that you just import the namespace and get all the stuff in it, this is popular in Java for example, however in C++ this is a bit fraught because while you can import a namespace, you actually get everything from that namespace and all containing namespaces.
Because the C++ standard library defines a huge variety of symbols, if you do this you get almost all of those symbols. Most words you might think of are in fact standard library symbols in C++, std::end, std::move, std::array, and so on. So if you imported all these symbols into your namespace it's easy to accidentally trip yourself, thus it's usual not to do so at all.
Another option would be to have some magic that introduces certain commonly used features, Rust's preludes do this, both generally (having namespaces for the explicit purpose of exporting names you'd often want together) and specifically ("the" standard library prelude for your Edition is automatically injected into all your Rust software by default). C++ could in principle do something like this but it does not.
Leave un-namespaced identifiers to those that are declared in the current file and namespace everything else. If you really want, you’re free to add “using namespace std” or otherwise alias the namespace, but keeping standard library functions out of the global namespace as a default is a good thing! (In any language, not just C++)
You're free to, but I discourage the habit. It's more verbose to add the namespace:: prefix to symbols, but it sure does make it easier on the devs that have to work with the code later.
C++ has been my main language for a very long time, but I've been a grumpy skeptic of C++ since around C++14 due to the language spec's total complexity. So I've mostly stuck with C++11.
But now that C++ has modules, concepts, etc., I'm starting to wonder if C++23 is worthwhile for new projects. I.e., the language-spec complexity is still there, but the new features might tip the balance.
I'd been thinking to walk away from C++ in favor of Rust for new projects. But now I might give C++23 a chance to prove itself.
To do that, it can only add features and never take them away. Instead, it adds features that deprecate the practice of PITA patterns that were common, necessary and difficult.
Like, SFINAE was necessary all over the place to make libraries "just work" the way users would expect. But, it is a PITA to write and and PITA to read. Now, constexpr if and auto return types can usually collapse all that scattered, implicit, templated pattern matching down to a few if statements. Adding those features technically made the standard more complicated. But, it made new code moving forward much simpler to understand.
Similarly: Before variadic templates, parameter packs and fold expressions, you had the hell of recursive templates. Auto lambdas make a lot of 1-off templates blend right into the middle of regular code. Deduction guides set up library writers to set you up to write
std::array names{"Alice", "Bob", "Charlie};
instead of std::array<const char*, 2> names{"Alice", "Bob", "Charlie};There is a very wide middle ground between C++'s "Your horrific unsafe code from the 80s still compiles" and Python's "We changed the integer values common operations on strings return at runtime with absolutely no way to statically tell how to migrate the code".
In Dart, we moved to a sound static type system in 2.0, moved to non-nullable types in 2.13 (sound and defaulting to non-nullable!), and removed support for the pre-null safety type system in 3.0. We brought almost the entire ecosystem with us.
Granted, our userbase is much smaller than C++'s and the average age of a given Dart codebase is much younger.
But you can deprecate and remove old features without causing a decade of misery like Python did. You just need good language support for knowing which version of the language a given file is targeting, good static typing support, and good automated migration tooling. None of those is rocket science.
That's way easier to do in a naturally statically-typed language though.
C++, Java and .NET are now all living through "decade of misery like Python did", exactly because not everyone is jumping into the latest versions of the languages and ecosystem changes.
That is, in Java you may not be able to compile old code with new javac, but the class file format is still understood by the JVM. So your old .jar still works.
I believe it is the same in C++ with the .so. I don't know about .NET.
In Python however, I don't think the .pyc were compatible between Python2 and Python3.
Same applies to other examples.
The result being that programmers have to learn every single one of those ways of doing things in order to read your coworkers code. Give me python 3 breaking changes any day
this is a problem, btw AFAIK c++ modernizations are few and rarely successful.
for(std::vector<sometype<int>>::const_iterator it = my_container.cbegin(); it != my_container.cend(); ++it){
// use *it
}
You could use: for(auto const& val : my_container) {
// use val
}
Which really paved the way for less hesitation around nesting templates and other awkward things.Same goes for map with the introduction of structured bindings:
for(std::map<somekey, sometype<int>>::const_iterator it = my_map.cbegin(); it != my_map.cend(); ++it){
// use it->first, it->second
}
became for(auto const& [key, val] : my_map) {
// use key, val
}
The introduction of std::variant made state a lot easier to reason about. Unions meant too much room for error, and cramming polymorphism into places it shouldn't have ever been was riddled with its own performance and memory issues.std::optional was also a game changer in that regard.
constexpr was huge. It killed off a bunch of awkward template metaprogramming for something faster to compile and easier to deal with. I wasn't a part of the consteval discussions but from what I HEARD people say about it and what it actually WAS, I think it was castrated at some point along the way.
Concepts, also a big one.
Formatting (RIP iostreams) is much nicer than the alternative.
Ranges, coroutines, modules - they'll have their day too.
And we have a bunch of stuff to look forward to. Senders and receivers solves a lot of component interaction problems. Reflection will rid us of so much annoying provisioning of classes. Pattern matching and expression statements will be major usability boosts.
Ok, C++ doesn't move at the speed of newer languages, but for its size, complexity and all of the hurdles that it has on the way to standardisation, I think modernisations are many and often successful.
I would absolutely love a major break that fixed things we can't currently fix due to ABI risks, implementer resistance, etc. I'd love to rid us of the old ways™ and focus on safety the right way™ - by removing bad bits rather than just adding good bits and telling people to use them and follow a bunch of guidelines. I'd love better defaults.
There is so much work to do to make C++ great again puts on MCPPGA hat, but I can't agree that modernisation are few and rarely successful.
For example, we have std::variant, yay! So where's the pattern matching? Vomits from a glance
I guess I’m just right at home and therefore a bit reluctant to jump into C++20. That and the constantly changing “””correct way of doing things.”””
Don't forget it isn't only the language, the various implementations, standard library, breaking changes, and major 3rd party libraries.
So I embrace complexity, as the alternative is being stuck with something like Go.
But really, and I say this from the perspective of someone in the embedded world who still does most of his serious work in C and cares about code generation and linkage: I think the whole concept of these Extremely Heavy Systems Programming Languages is not long for this world. In modern systems, managed runtimes a-la Go/Swift/JVM/.NET and glue via crufty type-light environments like Python and Javascript are absolutely where the world has ended up.
And I increasingly get the feeling that those of us left arguing over which monstrosity to use in our shrinking domain are... kinda becoming the joke.
Eh. I don't know if I agree. I've worked on a few large C++ codebases, and the cognitive load between Rust and C++ is incomparable.
The ownership/borrowing stuff is complexity you deal with implicitly in other systems languages, here's it's just more explicit and semi-automated most of the time by the compiler.
In C++ the terse thing is never the correct thing. If I'm using metaphors: a Rust sentence, in C++ usually has to be expressed through one or more paragraphs. The type system is so loose you have to do "mental" expansions half the time (or run the build and pray for no errors, or at the very least that the error is somewhat comprehensible[1]).
There's some low-level stuff that can be ugly (for various definitions of ugly), but that's every language. The low level bits of async are a bit wired, but once the concepts "click" it becomes fairly intuitive.
At least the ugly parts are cordoned behind library code for the most part, and rarely leak out.
I guess it could just boil down to familiarity, but it took me much less time to familiarise myself with Rust than it took me to familiarise myself with C++. We're talking months vs years to consider myself comfortable/adept at it. Although, maybe just some C++ or general programming wisdom transferred over?
[1]: This happens in Rust too, I must admit. But it's usually an odd situation that I've encountered once or twice with some very exotic types. In C++ it's the norm, and usually also reported with bizarre provenance
Really, the ergonomics of both the language features and standard tooling meant it always meant it was going to appeal to people and go places C++ would not, even if they in theory are both "systems languages" with large surface areas, and they overlap at this extreme end (kernels/embedded/firmware/etc) of the spectrum that little else fits into.
It does have a lot of developers saying good words for it whenever there is a chance in recent few years, but the reality is that, it's hard for most developers to learn, because of that it might remain to be a niche language for system programming.
c++ is not standing still, I feel since c++20 it becomes a really interesting modern language, and I hope its memory safety concern will be addressed over time at a faster pace, in fact if you use rule-of-zero, RAII, smart pointers etc correctly your code can be very much memory-safe already.
On what basis is this "reality" based? Recent survey of 1000 developers by Google denotes "[the] ramp-up numbers are in line with the time we’ve seen for developers to adopt other languages"[1]
> in fact if you use rule-of-zero, RAII, smart pointers etc correctly your code can be very much memory-safe already.
That's not what memory safe means. Memory safe means that even if you make a mistake, you cannot get a memory error, something that is true of Rust minus the `unsafe` superset. Even with what you describe, you can trivially get UB on C++ if you: keep a reference to an object, use a smart pointer after move (something that the compiler will not warn against), call .front on an empty vector, dereference an iterator that got invalidated by pushing in your vector, use a non thread-safe object in a multithreaded context. Rust will statically prevent a memory error from occurring in any of these situations.
If you're cautious, you may not get memory errors in a C++, but that's not "memory safe".
Also, as a C++ developer turned Rust developer, C++20 does nothing to capture my interest back. The module system is atrocious (orthogonality between modules and namespace, way too complex for what it wants to achieve, module partitions seriously?, poor tool support 3 years after the standard was published), there are no ergonomic sum types and pattern matching, no standard package manager (and the legacy compilation model based on textual inclusion makes build system more complex than they could be), no improvements on thread safety in sight (to be fair, you pretty much need your language to feature a borrow checker and be built around thread safety, it cannot be bolted on). I have little hope C++ is in capacity of addressing these issues in the next 6 years.
[1]: https://opensource.googleblog.com/2023/06/rust-fact-vs-ficti...
Nowadays C and C++ took the role of that inline Assembly, with higher level systems languages taking over the role of the former.
I mean, literally yesterday I had to stop and write a "device driver" (an MMIO firmware loader for an on board microcontroller) in Python by mapping /dev/mem. That's the kind of interface and development metaphor we'll be seeing in the longer term: kernel driver and interface responsibilities will shrink (they are already) in favor of component design done in pedestrian languages like Go or whatever that mere mortals will understand. There's no market for a big C++ or Rust kernel framework.
Modules were a great step forward for C++, but one of the features I enjoy the most about Rust is that adding a library just works. Especially for single person projects, which are on the smaller side, it feels absolutely great having to do near zero build management.
If you want to mess around with machine learning, learn enough Python for it.
Id you want to make a mobile app, learn Swift or Kotlin.
I get that this crowd will nerd out on language specifics.. but at the end of the day if we're good engineers we should use the best tool for the job.
I'm ambivalent about Rust, but its best feature compared to C++ is a universal package manager and build system. I like vcpkg well enough, but it's not Cargo, it can't be.
#![allow(dead_code)]
at the root of the crateI'm not sure what you mean by "optimizing away unused variables", so I'm interpreting it as variables that are literally unused after declaration.
I believe you're confusing Rust with Go. In Rust, an unused variable is just a warning, unless you explicitly asked the compiler to turn that warning (or all warnings) into an error.
If they're unused, then they're not helping with debugging either...
It's quite a bizarre reason
At least this is how it works in C and C++.
What you cannot have, is uninitialized variables which are used.
And as someone else said, if all you want is unused variables without warnings, you can say at the top of the root of the crate (`main.rs` or `lib.rs`):
#![allow(dead_code)]If we get a C++ Copilot as "borrow checker" that is already quite an improvement, even if not perfect.
Hmm, another idea: what if a language's compiler itself use an LLM to find invariants and issues (like race conditions) in the code?
In any case, taken to the extreme, the language won't matter any longer, beyond some druids with the knowledge to implement Star Trek like computing models.
We will be left to architect roles.
I took the approach of learning it as a brand new language that I knew nothing about, and especially avoiding thinking about what I already knew about C++ and especially C. The result was a language that yes, is a pleasure to work in.
Of course some context could not be ignored: there are about six ways to do many things because of back compatibility. But that back compatibility is why so many people use it. I write in one style, and just know the others for reading other people's code.
Users of 1996's Java won't recognise Java 21, or 2001's C# versus C# 12.
or, gcc can have its own c/c++ LSP, which is not the case.
Asking for a friend.
There's a series of talks called Back to Basics that seems to have quite a few talks every year where they discuss C++ features in general like move semantics or how testing works, etc. There have also been talks from the creators of CMake or the guys working on the recently added ranges proposal, so it does cover tooling as well.
I also quite enjoy following Jason Turner's C++ weekly series (https://www.youtube.com/@cppweekly) which also has quite a few episodes that are dedicated to new C++ features or new ways of handling old problems made available by the new features. They're generally quite short, each episode on average is 12 minutes.
Just looking down the list of videos I see this is also kind of a direct response to your question, from only 8 months ago. https://youtu.be/VpqwCDSfgz0 [ C++ Weekly - Ep 348 - A Modern C++ Quick Start Tutorial - 90 Topics in 20 Minutes ]
For experimenting:
https://gcc.godbolt.org/ is a tool called compiler explorer, which is a really good place to experiment with toy code.
It's a site where you can write some code and let it execute, to see the results, as well as see what ASM it compiles down to for various compilers.
That last feature really helped me figure out whether the compiler really does pick up an optimisation I'm trying out. (and it's how I got really impressed by how powerful constexpr is (that's one of the new keywords))
For References:
Generally the https://en.cppreference.com site is a really well maintained wiki that has good explanations of every concept and standard library feature.
It sticks to the standard and has use examples and is heavily interlinked, as well as some concept pages to give an overview of certain topics like overloading, templates, etc. (they also have a SFINAE article https://en.cppreference.com/w/cpp/language/sfinae)
You touch on kind of a lot here, and its pretty esoteric to even devs with 3-5 years experience. functors, operator overloading, namespaces, passing by reference to make the << chaining work, there is a lot you really have to know that is non-obvious. You can even jump into inheritance, templates and such if you really want to dive deep.
I thought this was normal after doing C++ for ~11 years, but when I finally broke out of the Stockholm syndrome and started a job working in other languages, I find it absurd how complex, or maybe a better way to put it is, how much language specific trivia you need to learn before you can even begin to unravel how a basic hello world! program really works.
My favourite C++ question is asking what std::move does. It's amazing the knots people twist themselves into explaining how it supposedly works, when in reality it's just a type cast to an r-value so that the move constructor/assignment gets called instead of the copy one.
You have to understand that it's a Sisyphean struggle to get people to use modern C++ features at all. You still see people passing around std::string instances by value and manually calling new and delete. They're ignorant of variants and think "final" is an advanced feature. I'm happy if they get as far as understanding even how to use CRTP.
There's a vast gulf in skill between the expert C++ practitioner who appreciates a blog post like the one linked and one who's been dragooned without training into writing some enterprise C++ thing. The latter relates to C++ as an inscrutable Lovecraftian god who might eat him slightly later if he makes the right cultist noises and does the right syntax dance.
``` class A { int f(this A & self, float b); }; ```
Type of &A::f is int ()(A &, float), not int (A::)(float).
This is huge for template metaprogramming function deduction if you stick to it because that generated a lot of template instantiations to cover all cases.
It feels a lot like the push_back -> emplace_back thing, where the feature has important use cases, but plenty of people will use it merely because they're mistaught that it's the "modern way" to do things without being told it's a footgun that bypasses existing safety features in the language, and it becomes impossible to hold back the flood. And they get away with it the majority of the time, but every now and then they end up introducing subtle bugs that are painful to debug.
But hey it's the shiny modern hammer, so obviously you can't just let it sit there. People can't just see a modern hammer and let you watch it sit there until you actually need it. You have to use it (and people will tell you to use it during code reviews) or everyone will look down on you for being so old-fashioned and anti-"progress".
I remember reading that clang was finally shipping std as a module, albeit experimentally. So this ought to be an interesting couple of years for C++ -- though I suppose it remains to be seen to what degree the ecosystem will keep up with all these changes vs using headers/exceptions/the traditional ways of doing things.
I have a medium sized JavaScript codebase that uses this (http://github.com/bhouston/behave-graph) and I could really use a port to C++ so that it can be integrated with the USD project, who has expressed interest.
I couldn't find an equivalent to async/await in my searches so I am fearful that porting this across to C++ is non-trivial.
But highly doubt you need anything like async/await for this kind of application. In fact, I'd go as far as to say async/await is almost never needed except for building networked services with truly massive performance demands.
If you need to do things in parallel for performance, you may be better off spinning up a threadpool (like asio's thread_pool) and using a threadsafe queue or similar (i.e. asio's asio::post()) to pass messages and get results.
Since you can do it with JS, you likely don't have massive perf requirements, and you just need to write it in simple, synchronous C++ and you may find it to be well fast enough.
We also have coroutines, they're async/await. Haven't tried them though.
As someone who has never used c++, it seems like it's primarily useful when latency and performance really matters and the cost of that is ease of use when it comes to developer quality of life/productivity.
I'd like to play with many gadgets, small embedded toys. Let's say it is custom a USB mouse. You may want to create a data structre that 1-1 represents whatever data you're pushing to the wire and the exact amount of time it takes you to push it over the wire.
C++ lets me abstract things better than C. There is a level of control in C++ where you get the decide to the minute detail how the compiler builds your software. With a C++ compiler you can say to the compiler to optimize one function, while not the other one in the same source file. Yes, the standard language abstracts the underlying machine and it will suprise you since compilers obey it very well if you not tell them otherwise. But all of the C++ compilers, including the niche ones, also let you pierce through those abstractions by special attributes and linker directives. So you can have an almost assembly code next to very abstracted and crazy optimized code.
C++ doesn't care how you build software, it is up to you to decide which kind of libraries, which libraries, what subarchitecture, what optimization level to use for each library. These sound crazy but current literature and internet is full of detailed documentation how to do these things since somebody at some point needed to do them to debug a particle accelerator or the latest PCIe bus or something and it is somehow useful for your stupid USB mouse.
Rust as the language (syntax, rules) is better. Rust articulates better, it makes a lot of sense. However to appeal the newcomers and people who look different sa(n/f)er ways to do things, Rust generally limits you. You need to codify certain "features", or you need build.rs files. You need to find underdocumented or "nightly" features. Whatever you come up with would be better formally defined. However going against the grain of Rust and Cargo creates abominations.
Notably a hobby http server, or a compiler, or an emulator, are projects that i enjoy/ed particularly and to which C++ is a good language choice, generally.
kv-api is a kv store with a REST api, for example, which is more unusual to write in C++.
[0]: https://github.com/lionkor [1]: https://gitlab.com/lionkor
Compiler development, in case you plan to play with GCC or LLVM as infrastructure.
Can't believe I used to use a third-party library ({fmt}) to do string interpolation like some kind of caveman.
I was too. Looking at [0], I'm guessing it's meant to bring these benefits over old-school "printf":
- Leverages the type system to avoid mismatches between the format string and the arguments.
- Adds Python- / Rust-style support for position-based substitutions, e.g. "{1}".
- Like Rust (I think; I'm still learning Rust) you can specify per-type formatters.
But I'm still confused about:
- How this relates to the standard library's iostream system. It seems pretty redundant.
- Why "println" exists.
print/println are based on the fmt library (https://github.com/fmtlib/fmt), which had it's first release in 2015, so it's roughly as old as Rust afaik. It's mainly inspired by Python formatting.
Having per-type formatters is just a logical conclusion of having type-safe formatting.
iostreams are for all kinds of io (which includes stdin/stdout), while fmt is entirely dedicated to formatting strings. Those things are related, but not the same. cout and cerr and such will probably be entirely superseeded by print/println (I hope), but it doesn't make iostreams generally redundant.
println adds a newline and you want to be able to choose, so there is print and println.
In C++20 if you wanted to print using std::format style formating (or it's variant) your options where:
```
std::cout << std::format("{}{}", arg1, arg2);// not the best syntax but less bad than everything else, slightly inefficient due to temp string
std::string tmp = std::format("{} {}", arg1, arg2); fwrite(stdout, tmp.c_str(), tmp.length()); // more ugly, and still uses temporary string
std::format_to(std::ostream_iterator<char>(std::cout), "{} {}", arg1, arg2); // avoids temp string, but what kind of monstrosity is this
```
But doesn't the std::format style formatting make the formatting part of ostream redundant -> it somewhat does. I guess that's why there are 3 types of print overloads:
* accepting ostream as first argument
* accepting c style "FILE*" as first output argument
* no output stream argument, implicitly outputting to stdout
One potential consideration to use ostream based versions instead of FILE* ones even though the formatting part is somewhat redundant, is RAII based resource management. If you want to use FILE* based versions, it means that you have to either remember manually close the File* handle which just like manual new/delete or malloc/free calls is something modern C++ tries to move away, or you have to create your own RAII wrappers around c style FILE* handles.
An alternative would have been introducing new kind of output stream API which only concerns with outputing buffered stream of bytes and skips the formatting part of ostream, but that would have introduced different kind of redundancy -> having 3 kinds of API for opening and writing to file. Allowing to pass ostream to print, while discouraging use of << operator parts of it seems like a simpler solution.
One more concern is how using the version of print which outputs to stdout without taking output File or ostream handle interacts with any buffering done by previous printf,and std::cout APIs and also synchronization with input streams. The same problem already existed before for interactions between printf and std::cout. For the most part if you don't mix them it doesn't matter too much, but if you care the standard defines how exactly they are synchronized. The new print probably either adds third case to this or is defined as being equivalent to one of the previous two.
After looking reading docs a bit more, seems like std::basic_streambuf/std::file_buf did exist. The new print API, might have used that as abstraction around output streams without some of the ostream formatting baggage. I have only seen them mentioned as implementation details of ostream stuff, never seen anyone use them directly.
There was also std::format_to in c++20 which avoid the temporary string std::format returns. I guess they could have extended that with additional overloads so that it can function more similar to std::print. But if they need to define new functions might as well call them something more familar to people coming from other languages like print, instead of having format_to(stdout, "formatstring", arg1, arg2);. Currently std::format_to outputs to output iterator.
So to sumarize why print exists: - combine std::format introduced by C++20 with outputting somewhere with cleaner syntax compared to doing it manually.
- sane looking hello world for beginner and people coming from other programming languages, this is probably also why println exists
- (maybe) avoid some heap allocations that temporary string returned by std::format would cause.
- avoid confusion of mixing two formatting approaches that `std::cout<<std::format()` implies
- avoid C style manual resource management that would be required for doing `File\* f=fopen("");auto tmp=std::format();write(f, tmp.c_str())`print("{:#04x}\n", 0xbeef);
- or -
cout << std::setw(4) << std::setfill('0') << std::hex << 0xbeef << std::endl;
I kind of wonder why anyone thought iostreams would be a good idea to begin with. I don't think anyone but C++ ever created a comparable interface.
1. Type safe string formatting. You'd get a compile error if the value you tried to write didn't support it.
2. User-defined formatting support for user defined types. You could make instances of your own class support being written directly to a stream. Sort of like how some other languages let you override `toString()`.
3. No runtime dispatch for #2. Unlike Java, C#, etc. the user-defined formatting code for your type is called statically with no overhead of virtual dispatch.
Using operator overloading to achieve all of those was really clever and led to a very powerful, efficient, and flexible system. It's also just unfortunately pretty verbose and kind of weird.
IDK, it'll probably make more sense in another 15 years as we clear away the cruft of all the things that tried to bring "cloud native" paradigms into a space where they didn't really fit...
The big thing is that they wanted type safe IO. Not like printf where you can print an integer with %s and the compiler won't have a problem with that, and it will crash.
Reusing bit shift operators for IO is quite clever actually. If you have operator overloading, you have type safe IO for free. Remember C++ came out in the 80s as a superset of C, these technical considerations mattered. std::println doesn't look like much, but it actually involves quite significant metaprogramming magic to work as intended, which is why it took so long to appear.
It's a miserable trap. Operators should do something in particular, because of the Principle of Least Surprise. The reader who sees A + B should be assured we're adding A and B together, maybe they're matrices, or 3D volumes, or something quite different, but they must be things you'd add together or else that's not a sensible operation for the + operator.
When you don't obey this requirement, the precedence will bite you as, as it does for streams. Because you forgot, these operations still get bit shift precedence even though you're thinking of this as format streaming it's just a bit shift and happens when you'd do a bit shift...
Streams looks like something dumb you'd do to show off your new operator overloading feature, because that is in fact what it is. It should have existed like Mara's whichever_compiles! macro for Rust - as a live fire warning, "Ha, this is possible but for God's sake never use it" - but instead it was adopted for the C++ standard library.
Iostream predates parameter packs, which is why they use << for a variadic API.
Just like Pascal/Modula-2, except 30 years later :-D
Now if they could fix the CaSe SeNSiTivity bug, get rid of macros, get some better strings, and maybe use @ to get an address, and ^ to point to things.... they'd be on to something.
Especially getting rid of macros, I hate them.
Most C code looks like line noise, maybe they could then use something like Begin/End to denote blocks instead of abusing comment markers {} ;-)
Now I still miss my favorite language, FP...