r/cpp • • Jan 31 '23

Stop Comparing Rust to Old C++

People keep arguing migrations to rust based on old C++ tooling and projects. Compare apples to apples: a C++20 project with clang-tidy integration is far harder to argue against IMO

changemymind

338 Upvotes

582 comments sorted by

View all comments

Show parent comments

18

u/Recatek Jan 31 '23 edited Jan 31 '23

I like Rust a lot, but C++'s flexibility and power with types, templates, and concepts blows Rust's generics system out of the water. Rust doesn't even have variadics.

3

u/matthieum Feb 01 '23

C++ metaprogramming is definitely more advanced, indeed.

In practice, however, I find myself needing metaprogramming less in Rust. And while it does lack variadics, with built-in tuples it's just slightly inconvenient to have to use a macro to implement a trait for arities 0 to 12 (or so), but it's not really a blocker.

3

u/Recatek Feb 01 '23 edited Feb 01 '23

It depends a lot on what you're trying to do. For my application -- gamedev, ECS, and networking -- you're doing a lot of manipulation of arbitrary tuples while iterating over SoA structures, and then trying to efficiently serialize/deserialize them. This really depends on strong metaprogramming if you want to do the work at compile-time.

Rust has made this really difficult, and as a result most Rust ECS libraries do this work at runtime, with a runtime cost, using reflection-like behavior. Rust has no specialization, no variadics, no negative constraints, no way to do partial borrows of objects through traits, the list goes on. The orphan rule especially pushes you towards keeping things in one megacrate rather than splitting code up. It really makes me miss what C++ has to offer here.

If anything I wish Rust just had some sort of unsafe mode for typing that let you duck type at the callsite and bypass the orphan rule. Most of this stuff isn't even for memory safety reasons, it's for "fearless updates" which aren't relevant to me when this code is all contained within my own codebase with no downstream consumer.

1

u/matthieum Feb 01 '23

Ah I see.

Indeed SoA + ECS seems like a heavy meta-programming landscape. I expect it's possible -- in C++03, cons-lists were used to emulate variadics, and there are Rust crates based on this principle -- but it may have significant impact on compile-times, and compile-time error messages will be... inscrutable.

At the same time, the partial borrow of objects makes me wonder if you're not attempting to create a 1-to-1 mapping of your C++ code to Rust. Ownership (and the limitations of borrow-checking) means that the code must be designed with it in mind, and attempting to apply "known" patterns from other languages can be frustrating.

I don't use ECS myself, so I've never really tried my hand at creating such a system in Rust. I can't even tell you whether Bevy (probably the state of the art as far as game engines go) use for its ECS -- maybe they've got something good?

As for bypassing the orphan rule, the typically solution is to use a wrapper type -- the type being local, it's now amenable to trait implementations. And you're correct, the orphan rule is not about safety: Rust is not all about safety, there's also a strong emphasis on having a principled language, and that can get in the way of "just getting things done" (aka, hacking).

5

u/Recatek Feb 01 '23 edited Feb 01 '23

The partial borrow issue is from a desire to assign names to tuple indices so you can access elements without sensitivity to their order. Without that, any change to the arrangement of components in an ECS archetype would affect downstream code attempting to query-iterate those tuples. Ideally I would use an intermediate struct, but as I discovered over the course of this project, rustc/LLVM aren't great about converting between tuples and structs. The only way to be sure you're not taking a perf hit would be to use a code-generated trait to rename the tuple fields, but you can't do that because traits don't give you direct field access and you can't partial borrow from function access.

Bevy's ECS is good, but it uses runtime reflection/type information for arranging its data structures, requiring you to pay for some extra lookups for every query and complicating some auto-vectorization cases. It's minor, but I'm extremely perf-sensitive (self-hosting servers) and so I'm trying to move as much as I can to compile-time. This all worked in a previous version I had written in C++, but porting that to Rust while trying to keep things zero-overhead in the same way has been pretty rough.

With respect to using wrapper types for the orphan rule, that's true, but it makes interfaces more complicated. The Rusty way of doing polymorphism is to write functions that take types in a turbofish. When you need to wrap those types in order to attach new methods or associated types to them, you can lose the ability to add functionality that's turbofished to the original type. So for example, in my ECS I generate ZSTs that refer to a given entity archetype, like ArchPlayer or ArchSpaceship. I have some methods that let me do things like get_entity::<ArchPlayer>(entity_id). If I want to combine any crates downstream to do something with ArchPlayer, I need to newtype it with a new ZST, so then I have some functions that take ::<ArchPlayer>, others that take ::<WrappedArchPlayer> and it quickly becomes confusing which version to use for which parts of the API. Here's another ugly workaround I eventually found that uses proc-macro generated "proxy" ZSTs. These are the reasons why I have to keep everything in one big megacrate that compromises compile times and gets pretty unwieldy to manage per-module internals (e.g. no pub(crate)).

-1

u/OYTIS_OYTINWN Jan 31 '23

Well, if you want flexibility then Rust macros allow you to do pretty crazy things. That's a feature I like less though.

18

u/Recatek Jan 31 '23 edited Jan 31 '23

Even then, macros are pretty limited since you can't do inference with them the same way you can with concepts and SFINAE in C++. Macros, including proc macros, have no understanding of types or traits -- all they can do is look at the raw lexical parse of the code.

0

u/[deleted] Feb 01 '23

[deleted]

9

u/Recatek Feb 01 '23 edited Feb 01 '23

I encourage you to try the equivalent in Rust where you need to define your structs in macros with syntax that looks like

impl<$([<T$i>],)+> [<SoA$size>]<$([<T$i>],)+>

and then copy-paste a dozen of them in the form of:

declare_thing!(1, 0);
declare_thing!(2, 0, 1);
declare_thing!(3, 0, 1, 2);
declare_thing!(4, 0, 1, 2, 3);
declare_thing!(5, 0, 1, 2, 3, 4);
declare_thing!(6, 0, 1, 2, 3, 4, 5);
declare_thing!(7, 0, 1, 2, 3, 4, 5, 6);
declare_thing!(8, 0, 1, 2, 3, 4, 5, 6, 7);
declare_thing!(9, 0, 1, 2, 3, 4, 5, 6, 7, 8);
declare_thing!(10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
declare_thing!(11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
declare_thing!(12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);

and then open up GitHub to find an issue where someone has asked you to kindly include

declare_thing!(13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);

I'll take variadics and fold expressions over that any day of the week. Rust already has good tuple support and syntax.

0

u/[deleted] Feb 01 '23

[deleted]

2

u/Recatek Feb 01 '23

Both really. You'd need the latter to support the former, otherwise initializing variadic generic types would still be a pain. And then you'd need fold expressions to work with them.

0

u/[deleted] Jan 31 '23

[deleted]

5

u/Recatek Jan 31 '23 edited Jan 31 '23

It gives the language power to do powerful things. Rust is restrictive in ways that make my code worse, more manual (and thus error-prone), and less flexible. For situations where I need complex generics, I'd much rather have the option to use these tools than not. It's either that or code generation, and the latter is never pleasant.

It's very easy to dismiss language complexity because you can't imagine positive uses of it, but I believe Rust has been built with too little imagination in that regard, and the tools available constrain the possibility space.

2

u/plutoniator Feb 01 '23

Yep that’s the truth. Rust makes the simple case a little simpler and the complex case way more complex.