Hacker Newsnew | past | comments | ask | show | jobs | submit | mgaunard's commentslogin

Reduced to 80km/h since 2018.

Oh right, I totally forgot! I mean, even then, for so many of those roads I'd never consider driving that fast haha

Paris' périphérique is nowadays limited to 50 km/h.

It's fundamentally different; Rust entirely rejects the notion of a stable ABI, and simply builds everything from source.

C and C++ are usually stuck in that antiquated thinking that you should build a module, package it into some libraries, install/export the library binaries and associated assets, then import those in other projects. That makes everything slow, inefficient, and widely dangerous.

There are of course good ways of building C++, but those are the exception rather than the standard.


"Stable ABI" is a joke in C++ because you can't keep ABI and change the implementation of a templated function, which blocks improvements to the standard library.

In C, ABI = API because the declaration of a function contains the name and arguments, which is all the info needed to use it. You can swap out the definition without affecting callers.

That's why Rust allows a stable C-style ABI; the definition of a function declared in C doesn't have to be in C!

But in a C++-style templated function, the caller needs access to the definition to do template substitution. If you change the definition, you need to recompile calling code i.e. ABI breakage.

If you don't recompile calling code and link with other libraries that are using the new definition, you'll violate the one-definition rule (ODR).

This is bad because duplicate template functions are pruned at link-time for size reasons. So it's a mystery as to what definition you'll get. Your code will break in mysterious ways.

This means the C++ committee can never change the implementation of a standardized templated class or function. The only time they did was a minor optimization to std::string in 2011 and it was such a catastrophe they never did it again.

That is why Rust will not support stable ABIs for any of its features relying on generic types. It is impossible to keep the ABI stable and optimize an implementation.


It's not true that Rust rejects "the notion of a stable ABI". Rust rejects the C++ solution of freeze everything and hope because it's a disaster, it's less stable than some customers hoped and yet it's frozen in practice so it disappoints others. Rust says an ABI should be a promise by a developer, the way its existing C ABI is, that you can explicitly make or not make.

Rust is interested in having a properly thought out ABI that's nicer than the C ABI which it supports today. It'd be nice to have say, ABI for slices for example. But "freeze everything and hope" isn't that, it means every user of your language into the unforeseeable future has to pay for every mistake made by the language designers, and that's already a sizeable price for C++ to pay, "ABI: Now or never" spells some of that out and we don't want to join them.


> It'd be nice to have say, ABI for slices for example.

The de-facto ABI for slices involves passing/storing pointer and length separately and rebuilding the slice locally. It's hard to do better than that other than by somehow standardizing a "slice" binary representation across C and C-like languages. And then you'll still have to deal with existing legacy code that doesn't agree with that strict representation.


If Rust makes no progress towards choosing an ABI and decides that freezing things is bad, then Rust is de facto rejecting the notion of a stable ABI.

Rust is just a bit less than 11 years old, C++ was 13 years old when screwed up std::string ABI, so, I think Rust has a few years yet to do less badly.

Obviously it's easier to provide a stable ABI for say &'static [T] (a reference which lives forever to an immutable slice of T) or Option<NonZeroU32> (either a positive 32-bit unsigned integer, or nothing) than for String (amortized growable UTF-8 text) or File (an open file somewhere on the filesystem, whatever that means) and it will never be practical to provide some sort of "stable ABI" for arbitrary things like IntoIterator -- but that's exactly why the C++ choice was a bad idea. In practice of course the internal guts of things in C++ are not frozen, that would be a nightmare for maintenance teams - but in theory there should be no observable effect from such changes and so that discrepancy leads to endless bugs where a user found some obscure way to depend on what you'd hidden inside some implementation detail, the letter of the ISO document says your change is fine but the practice of C++ development says it is a breaking change - and the resulting engineering overhead at C++ vendors is made even worse by all the UB in real C++ software.

This is the real reason libc++ still shipped Quicksort as its unstable sort when Biden was President, many years after this was in theory prohibited by the ISO standard† Fixing the sort breaks people's code and they'd rather it was technically faulty and practically slower than have their crap code stop working.

† Tony's Quicksort algorithm on its own is worse than O(n log n) for some inputs, you should use an introspective comparison sort aka introsort here, those existed almost 30 years ago but C++ only began to require them in 2011.


> C and C++ are usually stuck in that antiquated thinking that you should build a module, package it into some libraries, install/export the library binaries and associated assets, then import those in other projects. That makes everything slow, inefficient, and widely dangerous.

It seems to me the "convenient" options are the dangerous ones.

The traditional method is for third party code to have a stable API. Newer versions add functions or fix bugs but existing functions continue to work as before. API mistakes get deprecated and alternatives offered but newly-deprecated functions remain available for 10+ years. With the result that you can link all applications against any sufficiently recent version of the library, e.g. the latest stable release, which can then be installed via the system package manager and have a manageable maintenance burden because only one version needs to be maintained.

Language package managers have a tendency to facilitate breaking changes. You "don't have to worry" about removing functions without deprecating them because anyone can just pull in the older version of the code. Except the older version is no longer maintained.

Then you're using a version of the code from a few years ago because you didn't need any of the newer features and it hadn't had any problems, until it picks up a CVE. Suddenly you have vulnerable code running in production but fixing it isn't just a matter of "apt upgrade" because no one else is going to patch the version only you were using, and the current version has several breaking changes so you can't switch to it until you integrate them into your code.


This is all wishful thinking disconnected from practicalities.

First you confuse API and ABI.

Second there is no practical difference between first and third-party for any sufficiently complex project.

Third you cannot have multiple versions of the same thing in the same program without very careful isolation and engineering. It's a bad idea and a recipe for ODR violations.

In any non-trivial project there will be complex dependency webs across different files and subprojects, and humans are notoriously bad at packaging pieces of code into sensible modules, libraries or packages, with well-defined and maintained boundaries. Being able to maintain ABI compatibility, deprecating things while introducing replacement etc. is a massive engineering work and simply makes people much less likely to change the way things are done, even if they are broken or not ideal. That's an effort you'll do for a kernel (and only on specific boundaries) but not for the average program.


> First you confuse API and ABI.

I'm not confusing API with ABI. If you don't have a stable ABI then you essentially forfeit the traditional method of having every program on the system use the same copy (and therefore version) of that library, which in turn encourages them to each use a different version and facilitates API instability by making the bad thing easier.

> Second there is no practical difference between first and third-party for any sufficiently complex project.

Even when you have a large project, making use of curl or sqlite or openssl does not imply that you would like to start maintaining a private fork.

There are also many projects that are not large enough to absorb the maintenance burden of all of their external dependencies.

> Third you cannot have multiple versions of the same thing in the same program without very careful isolation and engineering.

Which is all the more reason to encourage every program on the system to use the same copy by maintaining a stable ABI. What do you do after you've encouraged everyone to include their own copy of their dependencies and therefore not care if there are many other incompatible versions, and then two of your dependencies each require a different version of a third?

> In any non-trivial project there will be complex dependency webs across different files and subprojects, and humans are notoriously bad at packaging pieces of code into sensible modules, libraries or packages, with well-defined and maintained boundaries.

This feels like arguing that people are bad at writing documentation so we should we should reduce their incentive to write it, instead of coming up with ways to make doing the good thing easier.


I would suggest importing binaries and metadata is going to be faster than compiling all the source for that.

You'd be wrong. If the build system has full knowledge on how to build the whole thing, it can do a much better job. Caching the outputs of the build is trivial.

If you import some ready made binaries, you have no way to guarantee they are compatible with the rest of your build or contain the features you need. If anything needs updating and you actually bother to do it for correctness (most would just hope it's compatible) your only option is usually to rebuild the whole thing, even if your usage only needed one file.


"That makes everything slow, inefficient, and widely dangerous."

There nothing faster and more efficient than building C programs. I also not sure what is dangerous in having libraries. C++ is quite different though.


Of course there is. Raw machine code is the gold standard, and everything else is an attempt to achieve _something_ at the cost of performance, C included, and that's even when considering whole-program optimization and ignoring the overhead introduced by libraries. Other languages with better semantics frequently outperform C (slightly) because the compiler is able to assume more things about the data and instructions being manipulated, generating tighter optimizations.

I was talking about building code not run-time. But regarding run-time, no other language does outperform C in practice, although your argument about "better semantics" has some grain of truth in it, it does not apply to any existing language I know of - at least not to Rust which is in practice for the most part still slower than C.

ODR violations are very easy to trigger unless you build the whole thing from source, and are ill-formed, no diagnostic required (worse than UB).

Neither "ODR violations" nor IFNDR exist in C. Incompatibility across translation units can cause undefined behavior in C, but this can easily be avoided.

C simply has less wording for it because less work has been put into it.

The same problems exist.


The ODR problem is much more benign in C. Undefined behavior at translation time (~ IFNDR) still exists in C but for C2y we have removed most of it already.

>There are of course good ways of building C++, but those are the exception rather than the standard.

What are the good ways?


"Do not do it" looks like the winning one nowadays.

Build everything from source within a single unified workspace, cache whatever artifacts were already built with content-addressable storage so that you don't need to build them again.

You should also avoid libraries, as they reduce granularity and needlessly complexify the logic.

I'd also argue you shouldn't have any kind of declaration of dependencies and simply deduce them transparently based on what the code includes, with some logic to map header to implementation files.


The problem is doing this requires a team to support it that is realistically as large as your average product team. I know Bazel is the solution here but as someone who has used C++, modified build systems and maintained CI for teams for years, I have never gotten it to work for anything more than a toy project.

I have several times built my own system to do just that when it wasn't even my main job. Doesn't take more than a couple of days.

Bazel is certainly not the solution; it's arguably closer to being the problem. The worst build system I have ever seen was Bazel-based.


> I have several times built my own system to do just that when it wasn't even my main job. Doesn't take more than a couple of days.

Really? I'd love a link to even something that works as a toy project

> Bazel is certainly not the solution; it's arguably closer to being the problem. The worst build system I have ever seen was Bazel-based.

I agree


It usually ends up somewhat non-generic, with project-specific decisions hardcoded rather than specified in a config file.

I usually make it so that it's fully integrated with wherever we store artifacts (for CAS), source (to download specific revisions as needed), remote running (which depending on the shop can be local, docker, ssh, kubernetes, ...), GDB, IDEs... All that stuff takes more work for a truly generic solution, and it's generally more valuable to have tight integration for the one workflow you actually use.

Since I also control the build image and toolchain (that I build from source) it also ends up specifically tied to that too.

In practice, I find that regardless of what generic tool you use like cmake or bazel, you end up layering your own build system and workflow scripts on top of those tools anyway. At some point I decided the complexity and overhead of building on top of bazel was more trouble than it was worth, while building it from scratch is actually quite easy and gives you all the control you could possibly need.


This is all great, but it doesn’t sound simple or like 200 lines of code.

>Build everything from source within a single unified workspace, cache whatever artifacts were already built with content-addressable storage so that you don't need to build them again.

Which tool do you use for content-addressable storage in your builds?

>You should also avoid libraries, as they reduce granularity and needlessly complexify the logic.

This isn't always feasible though.

What's the best practice when one cannot avoid a library?


You can use S3 or equivalent; a normal filesystem (networked or not) also works well.

You hash all the inputs that go into building foo.cpp, and then that gives you /objs/<hash>.o. If it exists, you use it; if not, you build it first. Then if any other .cpp file ever includes foo.hpp (directly or indirectly), you mark that it needs to link /objs/<hash>.o.

You expand the link requirements transitively, and you have a build system. 200 lines of code. Your code is self-describing and you never need to write any build logic again, and your build system is reliable, strictly builds only what it needs while sharing artifacts across the team, and never leads to ODR.


In my experience, no one does build systems right; Cargo included.

The standard was initially meant to standardize existing practice. There is no good existing practice. Very large institutions depending heavily on C++ systematically fail to manage the build properly despite large amounts of third party licenses and dedicated build teams.

With AI, how you build and integrate together fragmented code bases is even more important, but someone has yet to design a real industry-wide solution.


Speedy convenience beats absolute correctness anyday. Humans are not immortal and have finite amount of time for life and work. If convenience didn't matter, we would all still be coding in assembly or toggling hardware switches.

C++ builds are extremely slow because they are not correct.

I'm doing a migration of a large codebase from local builds to remote execution and I constantly have bugs with mystery shared library dependencies implicitly pulled from the environment.

This is extremely tricky because if you run an executable without its shared library, you get "file not found" with no explanation. Even AI doesn't understand this error.


The dynamic linker can clearly tell you where it looks for files and in which order, and where it finds them if it does.

You can also very easily harden this if you somehow don't want to capture libraries from outside certain paths.

You can even build the compiler in such a way that every binary it produces has a built-in RPATH if you want to force certain locations.


That is what I'm doing so I can get distributed builds working. It sucks and has taken me days of work.

It's pretty simple and works reliably as specified.

I can only infer that your lack of familiarity was what made it take so long.

Rebuilding GCC with specs does take forever, and building GCC is in general quite painful, but you could also use patchelf to modify the binary after the fact (which is what a lot of build systems do).


> I can only infer that your lack of familiarity was what made it take so long

Pretty much.

Trying to convert an existing build that doesn't explicitly declare object dependencies is painful. Rust does it properly by default.

For example, I'm discovering our clang toolchain has a transitive dependency on a gcc toolchain.


The Mars Polar Lander and Mars Climate Orbiter missions would beg to differ.

(And "absolute" or other adjectives don't qualify "correctness"... it simply is or isn't.)


What about hyphenization?


Coroutines is just a way to write continuations in an imperative style and with more overhead.

I never understood the value. Just use lambdas/callbacks.


> Just use lambdas/callbacks

"Just" is doing a lot of work there. I've use callback-based async frameworks in C++ in the past, and it turns into pure hell very fast. Async programming is, basically, state machines all the way down, and doing it explicitly is not nice. And trying to debug the damn thing is a miserable experience


You can embed the state in your lambda context, it really isn't as difficult as what people claim.

The author just chose to write it as a state machine, but you don't have to. Write it in whatever style helps you reach correctness.


You still need the state and the dispatcher, even if the former is a little more hidden in the implicit closure type.

Not necessarily. A coroutine encapsulates the entire state machine, which might pe a PITA to implement otherwise. Say, if I have a stateful network connection, that requires initialization and periodic encryption secret renewal, a coroutine implementation would be much slimmer than that of a state machine with explicit states.

> Just use lambdas/callbacks.

Lol, no thanks. People are using coroutines exactly to avoid callback hell. I have rewritten my own C++ ASIO networking code from callback to coroutines (asio::awaitable) and the difference is night and day!


I'll take the bait. Here's a coroutine

    waitFrames(5); // wait 5 frames
    fireProjectile();
    waitFrames(15);
    turnLeft(-30/*deg*/, 120); // turn left over 120 frames
    waitFrames(10);
    fireProjectile();
    // spin and shoot
    for (i of range(0, 360, 60)) {
      turnRight(60, 90);  // turn 60 degrees over 90 frames
      fireProjectile();
    }
10 lines and I get behavior over time. What would your non-coroutine solution look like?

Given a coroutine body

``` int f() { a; co_yield r; b; co_return r2; } ```

this transforms into

``` auto f(auto then) { a; return then(r, [&]() { b; return then(r2); }); }; ```

You can easily extend this to arbitrarily complex statements. The main thing is that obviously, you have to worry about the capture lifetime yourself (coroutines allocate a frame separate from the stack), and the syntax causes nesting for every statement (but you can avoid that using operator overloading, like C++26/29 does for executors)


How is this better than the equivalent coroutine code? I don't see any upsides from a user's perspective.

> The main thing is that obviously, you have to worry about the capture lifetime yourself

This is a big deal! The fact that the coroutine frame is kept alive and your state can just stay in local variables is one of the main selling points. I experienced this first-hand when I rewrote callback-style C++ ASIO code to the new coroutine style. No more [self=shared_from_this()] and other shenanigans!


Using shared_ptr everywhere is an antipattern.

The whole point of controlling the capture is controlling the memory layout, which is what C++ is all about.

Even with Asio, you don't really have to do this. It's just the style the examples follow, and Asio itself isn't necessarily the best design.


With callbacks you have to make sure that your data persists across the function calls. This necessarily requires more heap allocations (or copies) than in a coroutine where most data can just live on the stack.

A coroutine doesn't do anything more than a callback does -- it's just syntactic sugar.

The default behaviour of many asynchronous systems is to extend the lifetime of context data until all the asynchronous handlers have run. You can also just bind them to the resource instead which is arguably more elegant, but which depends on how cancellation is implemented.


Any data that needs to survive across several callback invocations requires shared ownership. This means that the data must be allocated on the heap and probably also requires reference counting. With lambdas it's also easy to make mistakes, e.g. by capturing variables on the stack by reference.

In a coroutine everything can just live on the stack because the stack frame itself is kept alive across the asynchronous function calls.

Don't you see the big difference regarding lifetime management?


Not necessarily, I already explained how (resource binding).

Close the socket, all handlers are cancelled, context can be freed immediately. The socket owns the data.


> The socket owns the data.

What if there's more than one socket involved?


Isn't this basically what javascript went through with Promise chaining "callback hell" that was cleaned up with async/await (and esbuild can still desugar the latter down to the former)

This is literally what coroutines are, syntactic sugar to generate nested lambdas.

Except in C++ this removes a fair amount of control given how low-level it is.


You can structure coroutines with a context so the runtime has an idea when it can drop them or cancel them. Really nice if you have things like game objects with their own lifecycles.

For simple callback hell, not so much.


The value is fewer indirect function calls heap allocations (so less overhead than callbacks) and well defined tasks that you can select/join/cancel.

Did you read the article? As the author says, it becomes a state machine hell very quickly beyond very simple examples.

I just don’t agree that it always becomes a state machine hell. I even did this in C++03 code before lambdas. And honestly, because it was easy to write careless spaghetti code, it required a lot more upfront thought into code organization than just creating lambdas willy-nilly. The resulting code is verbose, but then again C++ itself is a fairly verbose language.

The Unity editor does not let you examine the state hidden in your closures or coroutines. (And the Mono debugger is a steaming pile of shit.)

Just put your state in visible instance variables of your objects, and then you will actually be able to see and even edit what state your program is in. Stop doing things that make debugging difficult and frustratingly opaque.


Use Rider or Visual Studio. Debugging coroutines should be easy. You just can't step over any yield points so you need to break after execution is resumed. It's mildly tedious but far from impossible.

The age check laws is just funneling money directly to VPN companies.

There's no way anyone sensible would give over their identity to dodgy websites. It's easier to just pretend to be in a different country.


What the hell do allocations even have to do with time to read from memory...

It seems all of these Rust performance warriors have no understanding of computer architecture fundamentals.


It's not rust per se. It's a human trait. People make uneducated assumptions about the topics all of the time, and it's completely normal since your have to bootstrap yourself somehow to be able to grow further. It's impossible to know everything. And it's also a difficult situation because you do have to arrive to some conclusion. I do think, though, that in this particular case, assumptions made are a little bit all over the place and the tone is overconfident.


If an allocation falls in a forest and never gets written to, did it really happen?

We don’t allocate things we don’t write to. Maybe if the question sounds stupid then you didn’t appreciate it, instead of other people being stupid.


The real question is why wouldn't you prefer the terminal way over silly GUIs?


Author seems to be a painter/journalist/art journalist—so the answer to that is the same as to the OP question: so far it's primarily been built out for programming, by and for software engineers, where it seems completely natural.


The author is an accomplished software engineer.


Then why do they call it "Terminal" (ie the macOS app) instead of "the terminal" (the concept)? I was baffled.


It's an Apple-user thing. It's not "my phone", it's "my iPhone". It's not my laptop, it's my MacBook. It's not my headphones, it's my AirPods. It's not my smart watch, it's my Apple Watch Ultra 3 Sapphire Gold Plated. It's not my terminal, it's the Terminal, the one to rule them all. Only plebs use non-branded terminals!


and he’s using a free version of chatgpt? and not publishing source prompts - so there is no way to replicate?


Does it support C++?


Ops people are typically more useful given you probably already have devs.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: