Learning Rust and C

I think Rust is harder than C. I had interest in system programming, I read learning rust is great but it has a tough learning curve. I picked up C and I'm finding C easier compare to rust. Since rust is modern, it's still of great priority. I'm thinking maybe have not gotten the right material to simplify rust especially for beginner in system programming. And I think I'm enjoying C because of its less abstraction. Wanna ask the rust experts in the house, how do you manage to master rust and write idiomatic codes.

1 Like

It is much easier to learn C than Rust, but it is easier to write reliable software in Rust than C.

19 Likes

After decades of using C and C++ and a bunch of other compiled languages I won't bore you with, I refute the idea that "Rust is harder than C".

Starting with the initial beginners "Hello world" program we see that Rust is already simpler.

When programs get bigger and one is in a swamp of sloppy type coercions, memory allocations, thread races, we soon find that creating a predictably reliable program in C gets very hard.

Thing is, in Rust or C, lifetimes of variables, multiple mutable access and so on are things one has to take care of or your program will crash or give randomly unexpected results. C gives no help in that and any such mistakes require hours of debugging after many failures after code has been released to the wild. Meanwhile Rust makes you confront those issues upfront, before your program will even compile.

So, if anything, the hard part of Rust is thinking about what your program will do and getting it to compile. The hard part of C is spending hours debugging it when random things happen years after you have forgotten how the code works.

Which difficulty do you want to suffer?

As for "idiomatic Rust" I'm not the right guy to advise. After a couple of years of Rust development it is still me writing C and letting the compiler slap me around until the code compiles. Or I'm writing as if for Javascript. Either way the results have been solid and reliable. If not pretty and maximally performant.

14 Likes

That's natural for a systems programming novice. You'll have to go through a couple of years of suffering from debugging mysterious, impossible-to-reproduce use-after-free bugs in order to truly appreciate that being in an inherently unreliable, flaky environment full of 50-year-old bear traps is much harder compared to having to satisfy non-trivial but well-documented constraints of a modern compiler.

"I can get it to compile in less time" is not nearly everything.

16 Likes

Just learning to recognize what's idiomatic in one language is a skill you learn over years of exposure. Meaning, you have to learn many languages and read a lot of code in those languages to get a feeling for what's idiomatic (it's basically what that word means). The more you learn the better you become at recognizing patterns.

And many languages (in particular the early ones, pre internet communities but not only) have developed several idioms, C is one of those.

2 Likes

I summarized the experience that flipped my view over to "Rust is more beginner-friendly than C" in this other thread. But I also feel @H2CO3's point is an important one. Perhaps pick one of C or Rust to implement a program of middling complexity for fun and learning, and then reimplement the program in the other language to compare how the experiences differ.

My view these days is that sound C is much harder than safe Rust (which is also sound, by design). It's just that there's nothing stopping from you writing unsound C -- in many cases, there's not even something warning you that you have done so! Unsound C will happily compile, run, and maybe work... or maybe not work in weird ways, or maybe crash.

(Writing sound unsafe Rust is another story.)

9 Likes

As other have already mentioned, C gives you the illusion of being easier. The hard reality is that writing correct/sound/memory-safe C programs is very difficult for a beginner. Only after many hard-learned lessons (i.e. thousands of hours spent debugging) you sort of learn how to avoid many of the C traps. And even then, occasionally some memory-related/type-casting bug may creep in if you are not very careful (e.g. writing code in a rush to meet an impossible deadline).

A personal note: 10 years ago, I thought C was a beautiful language, mainly due to its simplicity. I avoided C++ like the plague. Around 5 years ago I found out about Rust. It was daunting at first, but satisfying after several months of practicing (implementing little pet projects). Only then I started to recognize C's shortcomings. Recently I have been forced to work with a C/C++ code base (tensorflow), and I really wish it had been written in Rust.

My recommendation is this: be patient with Rust. Put the time needed to learn the most useful language features. Find simple projects that interest you and implement them in Rust. Given the choice, I rather spent the time learning the most useful Rust abstractions than debugging C code.

Cheers,

7 Likes

I feel like C can be helpful to understand how memory is organized, sort of like a "learning language". I know it hasn't been created for that purpose though, but I feel like if I hadn't worked in C, I would have a harder time understanding Rust.

Directly starting with understanding the memory model of Rust doesn't seem to be an easy task. :sweat_smile:

2 Likes

I'm curious to know how Rust hides or make difficult understanding how memory is organized compared to C?

Notionally Rust has local variables on the stack, just like C. Then we have items on the heap, admittedly C makes you uses malloc to do that but I don't see that making things easier to understand. Surely explaining how things like Vec and String work is hardly more difficult.

Of course, I say "notionally" because a modern optimizing compiler can undermine all that, local variables may not exist on the stack or exist all after the optimizer has been at it. C suffers from this as well. Basically memory is not used the way you might guess from the source code in either language.

I don't know about memory models. I never knew I needed one for decades. The C standard did not speak of such things. I believe it does now. Again Rust and C have the same problems.

1 Like

:slight_smile:

To give an example, look at this code:

impl<T: ?Sized> Arc<T> {
    /* … */
    pub unsafe fn from_raw(ptr: *const T) -> Self {
        unsafe {
            let offset = data_offset(ptr);

            // Reverse the offset to find the original ArcInner.
            let arc_ptr = ptr.byte_sub(offset) as *mut ArcInner<T>;

            Self::from_ptr(arc_ptr)
        }
    }
    /* … */
}

(Source)

Understanding this without having worked with structs in C isn't so easy (without first understanding how Rust layouts its memory, using #[repr(C)]).

I find it easier learning pointer arithmetic in C because you deal with it all the time and it's not "shadowed" by a (safe) reference system.

Rust's standard memory layout doesn't allow these operations at all, so it's a somewhat hidden detail to the programmer how the memory is organized. Of course, you still know there's the stack and the heap, but everything feels a bit more abstract in Rust than if you do it in C.

However, I might be biased since I learned C first. Perhaps it's same easy/hard to get into these details when you start from scratch by learning Rust. I don't know.

1 Like

I thought C was a beautiful language, mainly due to its simplicity. I avoided C++ like the plague.

...meanwhile, I reached for C++ with both hands. :slight_smile:

Leveraging destructors and scoping into the RAII pattern alone was worth any added complexity, but I found C++ had many other treasures, revealing an elegance and sophistication that C never aspired to.

When speaking of C++ I can see the "sophistication", as in one definition of the word: "the process or result of becoming more complex, developed, or subtle"

However I do not see the "elegance". As in one definition of that word "the quality of being pleasingly ingenious and simple; neatness."

Like you, back in the day, I reached for C++ with both hands. I totally fell for Stroustrup's book on the design of C++ back in the late 1990's. Where he explains in detail why every little crazy feature of C++ just had to be that way. Obviously.

Over the years I started to think I was losing my mind. C++ did not help solve any problems I had. It only added layers of complexity to the solutions. Whenever I had an issue with C++ it seemed I was told it was my fault for not understanding the thing.

Amazingly, if you listen to Stroustrup's keynotes and presentations in recent C++ conferences you will hear him pleading with the standards committee to stop the ever expanding complexity of the language. He says things like "Inside C++ there is a much simpler language trying to get out"

Phew, it was not me losing my mind, Stroustrup himself feels the same way.

Then there was Rust.

7 Likes

C++ did not help solve any problems I had. It only added layers of complexity to the solutions.

I have to agree, C++ has presented an ever expanding attack surface to critics and, although I made extensive use of the boost libraries (especially lambda and spirit) in the early 2000's, I've felt little need to adopt any of the features added since C++11 (the auto keword was more than welcome).

Also, a lot of shops I've worked in have ruled out many of the advanced C++ features, even generics beyond the STL. I think one of the key mistakes people make with this language is feeling obliged to use every available feature. Instead, I think the breadth of the language should be seen as an opportunity to tailor a more domain specific subset.

So it's a stretch to suggest that C++ adds nothing useful to C. That's just wrong. Nor is (say) Java a 'better' (more efficient, safer, easier to use) language.

I'm here because I hope to learn and use Rust, because I'm drawn to it's promise of zero cost control over memory management and synchronisation. Truth is, while I know how to do this stuff effectively in C++, I generally avoid threading and prefer event loops. I also use a lot of Python and C, but I think Rust looks like a promising tool for systems programming.

As everyone knows, moving to a new language isn't simply a question of deciding the old one has had it's day. There's a reason that C++ has evolved, rather than being superceded: billions of lines of legacy code. The LLVM toolchain might free things up, but there's still a lot of inertia to overcome.

Also, I'm waiting to see functional languages find greater uptake in commercial development. It seems to take an average of twenty years for the features functional programmers take for granted (exception handling, lambda, continuations, ...) to worm their way into procedural languages. Even the key problem addressed by Rust (data integrity) is obviated by the 'read only' functional model.

Rust's innovation wasn't "data integrity" - it was "data integrity without needing any runtime impact". That's huge.

Also I find the continued C++ hate in here a bit tiresome. It was until Rust the only real contender for a language with the ability to abstract with zero cost: certainly others existed but not with enough of a "killer feature" to get the broad ecosystem support needed to responsibly use a language for anything other than a personal project. Further, C++ has had serious improvements from the committee since 08, and they are only getting more aggressive in their attempts to redesign and replace the flawed bits of the language.

Now that Rust is here, sure, I wouldn't use C++ over it for any new project, but that's more a measure of how good Rust is, not how bad C++ is. For me at least, no language better solves a problem than either Rust or JavaScript (counting Typescript) does, which is quite convenient for development, honestly.

4 Likes

Rust's innovation wasn't "data integrity" - it was "data integrity without needing any runtime impact". That's huge.

I agree, but I thought I'd already noted that with the words 'zero cost'...

Also I find the continued C++ hate in here a bit tiresome.

100%. That's what I was responding to. I didn't mean to suggest that newer features in C++ aren't useful, just that I haven't felt a need for them. Maybe this is just because I don't understand them, but it's sometimes because they don't mesh well with an existing code base.

Also, there are C++ 'features' that have turned out not to be useful, such as exception guarantees in method signatures. As I said, it isn't compulsory to use the entire language set in every project. It's necessary to understand and evaluate these features before they get baked in.

Fair enough, I was responding to this bit :

Which was a fair bit away from talking about Rust being "zero cost". There are plenty of languages that solve data integrity through different mechanisms (Erlang, for example), so this phrasing really minimizes the "killer feature" that is probably the main reason Rust is as popular as it is (though it really helps that it is actually a good language!)

There are plenty of languages that solve data integrity through different mechanisms

Yep, and few offer what Rust does. Erlang, for example, doesn't actually share data, it uses message queues. This is an approach I happen to like, but it comes with some real costs. I see Rust as providing a unique and highly desirable alternative. :slight_smile:

Rust requires putting a lot of things in code that C programmers have to put in documentation or in their heads, which means more work upfront but a lot less work down the line.

Take some C function signature like:

char * do_something(char *a, char *b) { ... }

What do we know about the pointer it returns? Is it something relative to "a" (and shouldn't outlive "a")? Is it relative to "b"? Is it something heap-allocated that needs to be freed later? Can it be NULL? There's no way to know just by looking at it, since C doesn't offer any way to specify that information.

Whereas comparable Rust signatures look like:

fn do_something<'a>(a: &'a str, b: &str) -> &'a str { ... }

fn do_something<'b>(a: &str, b: &'b str) -> &'b str { ... }

fn do_something(a: &str, b: &str) -> String { ... }

fn do_something<'a>(a: &'a str, b: &str) -> Option<&'a str> { ... }

Which are more complicated than the C version, but that extra information tells the compiler about our intent which it can then enforce.

6 Likes

Rust requires putting a lot of things in code that C programmers have to put in documentation or in their heads, which means more work upfront but a lot less work down the line.

That's because C is effectively assembler, in shorthand.

What do we know about the pointer it returns?

It's simple. We know:

  1. it's the address of some memory.
  2. the memory holds sizeof(char) bytes of possibly meaningful data.

Surely that's enough! :laughing:

Ok, it's (slightly) more complicated than that these days, but the point is that C can be read more or less transparently, in terms of operations on an actual machine. That can be very helpful if the machine is something you've just soldered together on a piece of veroboard, but it leaves the handling of a lot of messy details up to the programmer, so it doesn't scale well to, say, implementing a deep learning algorithm.