6 ms·
One of the common pitfalls I've seen in my time is someone writing a language they are familiar with in a language that just doesn't fit; trying to apply idioms
by EliRivers 1y ago
One of the common pitfalls I've seen in my time is someone writing a language they are familiar with in a language that just doesn't fit; trying to apply idioms that flow well with one language to another language where that's just not a good way to achieve the same ends.
An example I've seen a lot is a C thinker writing C++ classes with an init() function; sure, it works, but the C++ way is to do that in constructors. (All those about to start listing exceptions to that C++ idiom, please save it to the end, thanks!) The C thinker is still thinking about objects as "allocate memory, then set values" rather than the C++ way where allocation and initialisation are wrapped together into a single action (from the programmer's point of view).
So what are these pitfalls for a C++ thinker when writing Rust? This "phrasebook" is embracing the idea of taking a C++ way of thinking and applying it to Rust, which I'm sure will be fine for many situations, but what are the C++ phrases that are just the wrong way to do things in Rust?
- acje 1y agoA resource like this is a good place to discuss where the two languages are near and far. Of course there are going to be styles within each language that differ as much as the languages themselves.
- atq2119 1y agoTo be fair, there's a reason for the pattern with init methods you're describing. C++ constructors can't return values. If construction is fallible, the only way to communicate the error is via C++ exceptions. If you're in a code base that embraces exceptions, that's fine. But (C++) exceptions kind of suck, so many code bases don't, and then you have to find some alternatives. Over the years, I've increasingly adopted a pattern where the constructor is private in this case and the object construction can be done with static methods - which is a bit more like Rust, actually.
- legobmw99 1y agoThis is fine if the method you’re talking about is static — as you point out, it’s really all Rust has — but is absolutely a design mistake if it is not, which I think is what the poster above is referring to. It’s a common anti pattern and means you have an object that is at-best useless and at-worst completely broken after you call the constructor but before you call some member function on it
- TuxSH 1y agoTwo-phase initialization also has the added benefit of usually making the object have a constexpr constructor (usually a default constructor) and therefore making it eligible for constinit. That said, construct_at also exists.
- Conscat 1y agoNothing prevents std::vector from having a `constexpr` default-constructor except that it's not considered useful to do if you cannot follow that up with initializing its data in a constant context. For instance, this isn't very useful: constinit vector<int> v; But this would be more so: constinit vector<int> v(16, 1); // Fill with 16 1's. And the reason we can't do this wouldn't be solved by splitting it into multiple functions. EDIT: Actually, come to think of it, C++20's vector already supports the first example. It's just not used much that way because it's not very helpful. https://godbolt.org/z/avY4M9oMK https://godbolt.org/z/avY4M9oMK
- EliRivers 1y agoTo be fair, there's a reason for the pattern with init methods you're describing. Without prejudice on any other reasons, the most common reason for this pattern I've seen is people thinking in languages that basically don't have constructors, yet writing C++. It's not a good reason.
- Mond_ 1y agoHow would you deal with fallible construction of objects while avoiding exceptions in idiomatic C++?
- metaltyphoon 1y agoThis was one of the best decisions that Rust and Go did; not have constructors. In C# this is super annoying too, specially when you need an async operation to construct a type. This is usually done by having an private constructor and then using a static public method to create the type.
- Conscat 1y agoRust and Go have no form of a conversion operator (even if not a constructor), which makes scripting a type system essentially impossible. Numeric libraries in both of those languages are extremely cumbersome, largely for this reason.
- tialaramex 1y agoI don't understand this comment at all. Rust not only has the 'as' operator for this exact purpose, but it also has the suite of traits From, Into, TryFrom and TryInto for the infallible and fallible conversions respectively.
- Conscat 1y agoAs is an infix operator, it can never be invoked implicitly to draw these relationships between different types.
- Conscat 1y ago> the object construction can be done with static methods I've done that a lot too, but I found that free functions are much better for this than static member functions, because you can't get CTAD from static member functions. For example, with constructors we could write: vector{1, 2, 3}; // deduces vector<int> And with a static member, we would need: vector<int>::init(1, 2, 3); With a free function, we could write: make_vector(1, 2, 3); // returns vector<int>
- fooker 1y agoRight, when construction is fallible you need a factory. Constructors are called in surprising places when running a C++ program. For example, think of a copy constructor failing somewhere far away from the code you are writing. If C++ allowed construction to fail, the control flow of propagating these errors would be tedious and invasive. Hence exceptions as the only way to fail in a construction.
- pjmlp 1y agoI hate with passion two phase initialisation, C++ libraries that are bare bones C libraries wrapped in an extern "C" { }, malloc()/free(), C style coding and such.
- pornel 1y agoThe worst pitfall is Rust references == pointers. They are implemented as pointers, but their role is to give temporary (often exclusive) access that is restricted to a statically know scope, which is pretty specific and fits only some uses of some pointers/C++ references. In C++ pointers typically mean avoiding copying, but Rust references avoid storing/keeping the data. When these goals don't overlap, people get stuck with a dreadful "does not live long enough" whack-a-mole.
- trealira 1y ago>their role is to give temporary (often exclusive) access that is restricted to a statically know scope, which is pretty specific and fits only some uses of some pointers/C++ references You could have a vector of references to heap allocated data, as long as the references were parametrized by the same lifetime. You might do this if implementing a tree iterator using a vector as a stack, for instance. That goes beyond a statically known scope. But implementing a mutable iterator the same way would require a stack of mutable pointers (and therefore unsafe code whenever you dereference them), since mutable references have to be unique. That does seem like a bad limitation.
- hardwaregeek 1y agoThe rationale is probably that it’s better for C++ devs to write non idiomatic Rust than to keep writing unsafe C++. Like unless they use unsafe and completely circumvent the borrow checker, it’s still gonna be safer. Not letting perfect be the enemy of good and all. Plus idiomatic rust isn’t that strict a definition. Clippy will guide you for most of the simple stuff and the rest isn’t always worth following. Like people who try to do stuff “correctly” with traits often end up with way more complexity than it’s worth.