2:07 A refresh on C 4:04 Comments 4:18 Variables and structs 5:02 Primitive types 6:18 Functions 7:28 C++ is not C 8:11 Struct initialization 12:16 Struct initialization: sokol gfx 13:52 Modern C 14:54 C11 Additions 15:18 C11 Additions: static asserts 15:40 C11 Additions: _Generic and overloading 16:30 C11 Additions (cont) 17:06 Awesome macros 17:32 Awesome macros: defer 20:20 API Design: Math 21:18 API Design: Math in Modern C 23:18 API Design: Error handling 24:15 API Design: Error handling in Modern C 30:33 Generic APIs in C 31:40 Generic APIs in C: Dynamic Arrays 33:56 Generic APIs in C: Map 34:24 Libraries in C 37:06 When writing libraries 38:30 Memory Management 40:14 Temporary allocators 41:31 API Design: Modern C 42:15 String handling in C 42:54 Avoid libc 43:30 Replacing libc functionnality: printf 46:30 Case study: std::string 49:10 String handling in Modern C 51:50 String handling in Old C 53:40 String handling in Modern C (cont) 54:35 String handling in Modern C: another awesome macro (iterator) 55:41 First optimization at CA 58:40 Conclusion
I write C every day as an embedded developer. It's funny to me that some people think C is some ancient obscure language. C is not going anywhere for a long time.
@@kuhluhOG you do realize that us embedded developer write C99 when we're lucky, right? because most compilers are C90+extensions, some are C99+extensions. And the "slow" update cycle may be a sign that the language is mature and doesn't need new cool features/is stable/[insert another big advantage of C] gcc for arm is probably C11 now but how many are using C11 features, really?
Exactly. I have been using C for the last 24 years. All my embedded work is in C. At Apple we used C exclusively for all the systems work including the OS, the Apple car and all its systems.
Great talk, Luca! I'm definitely looking forward to a v2.0 for C23! :) I just finished developing a massive GStreamer plugin with ~10k lines of code. It's amazing how alive and exciting C programming is, and I'm a Gen-Z girl ;) While I work with other languages professionally, coding in C feels like an entertaining intellectual challenge. It also doesn't feel like you're doing something outdated, like when I had to translate some old Cobol scripts. No kidding, C is alive and long live C! I noticed some great comments about tools like valgrind and the cflags sanitizer. I loved that Luca mentioned defer, I also sometimes use bdwgc (the garbage collector) and libcsptr (smart pointers) in the mix! Keep up the great work!
This is probably the best argument for C: Implement your own libraries. The result: well defined, portable, benchmarked and optimized, *standard compliant* (ironically), source code.
This talk is great!, I was searching for a new language close to C, but this just reminded me the power of C. I love these ideas which keep the language updated.
Ditto - I haven't touched C since the 80s and K&R 1. It was all pretty painful back then - even the tooling was a challenge. Modern C with modern tooling, such as the new VS interactive debugger, seems a much more attractive proposition.
People often argue that writing C code is quite dangerous because of buffer overflows and memory leaks, however there are tools which can help to detect the majority of those problems e.g. valgrind. I'll never go back writing a C program without valgrind.
As if there is any need to use unprotected manually managed buffers - there are so many good libraries out there and even just writing a simple wrapper-struct takes only a few minutes. You only use the lowest level stuff when you either have no clue or when you really need it (tight memory constraints, high performance, low latency - that kinda stuff).
Good talk. I too find modern c to be really refreshing, and use it for personal projects. Also great to see sokol-gfx, which is a joy to use. The biggest problem I see with C is its lack of standard library. It makes the language inaccessible to people for quick "getting to know the language"-type of projects. Often the only alternative to using the standard library is either writing your own libraries or searching for other peoples libraries. But without significant experience you just can't judge the libraries you find properly, and certainly can't construct a good one. So beginners will use the standard-library, which is in many places crappy and very insufficient. Especially string handling, which is also probably one of the first thing people want to do with a language. Imo the top priority for any C "comittee" or people interested in pushing the language should be to band together and create a new defacto standard library based on modern c which everyone new then can simply use to get some stuff done.
I completely agree. C with a well designed and useful standard library would be amazing. It doesn't need to be huge, but it should have the basic building blocks such as strings, vectors, maps, etc. I don't like OO, but sometimes I use C++ just because I love the STL.
You don't need parentheses around a return value. It's return ; which can have parentheses around ,but return isn't a function. return(1)+2 returns the integer 3.
c is still one of the best languages around. it simple, incredible powerrfull and the fastest language there is. i have prigrammed hava since ot was called oak and now a bit c# and alm the others but c is still king. if you want to write code for a nuclear powerplabt, space probe or the kernel for your phone, c is the labguage to use.
Fixing strings in C is actually hard, although representing strings as { data, size } makes a lot of sense, the problem is when it comes to interoperability with legacy APIs and external libraries, where zero-terminated strings are usually expected, in which case such a string view needs to be reallocated and appended with a zero just for the purpose of passing it to some external API. So it's a matter of preference, one can either have cheap string views / slices, or a natural (zero-terminated) string representation, but not both at the same time. So there's always some trade off.
Yes, good point! Stupid question though: Cant we use a compatible_string that supports both null terminated via an extra pointer and the more handy {data, size} interface? Yes, it complicates stuff and might not be worth it for all cases, but maybe for most of them?
@@grafgrantula6100 str is just storing that null terminated string in its data field N calculating its length for the size So for str u can just return str->data to get back ur null terminated string
Hi, in regards to zero-terminated string API interoperability, just do what other langs do! You can have strategies where you copy the string to make it null terminated, or sometimes you can write your code such that you guarantee a string is null terminated. Also if you use a string_buffer rather than a string slice you can ensure the string_buffer always puts a null terminator at the end. A collection of strategies like this depending on the kind of program you write can certainly help and languages such as Zig are even better because the type system supports slices natively but also supports slices with sentinel values natively! So you can specify if a slice is meant to have a null terminator in the type system, pretty cool :D
You could have the underlying data just be a null-terminated string, represented by a { data, length } struct where the length is -1 to the underlying null-terminated string length. That way you get the more ergonomic interface where you have it, but you can also easily pass the string to legacy APIs and libs, as well as tap into the null-terminated C string *when you know what you are doing* by the convention that your string (or data pointer) is actually a null-terminated C string. All it costs is one convention and one extra byte per string, which is how C has it already anyway.
Even using Zig as a modern C compiler is quite nice. Cross-compilation and a unified build system you can write in Zig is a good bonus if you want to avoid the stack of tooling a typical C project needs.
@@ggss2836 a C compiler, a static analysis tool like valgrind, a debugger like gdb, a build system like make, cmake, meson, etc, a version control system very likely git, a way to track your dependencies like git submodules and/or your system's package manager
(at 43:19) math.h is not for libc, it's for libm ! And with tgmath.h, the math library is effectively generic (over long double, double, and float), which is *AWESOME*, and I think also part of modern C. But point taken that libc has issues.
He has eloquently voiced so many of my thoughts on C. The libc functions are RIDICULOUS. Also, strtok is pure comedy. Because of library design or build nightmares, C devs often have to re-implement very basic operations. If someone would design a sane, modern C std lib and a build system that could compete with cargo/npm/pip/etc... using C would not require any large tradeoffs from using other languages; and would have some very pronounced advantages, offering more control than any popular alternative. I did look into zig and rust; great languages, but those build times are not great. Idk if that's LLVM or just young compilers, but be prepared to get up and make yourself coffee every time you rename a variable.
Exactly, no one doing anything meaningful uses much of the standard C library. You have to be willing to build up your own more useful functions if you want to do anything non-trivial nowadays. But today’s culture is that of open-source dependencies where devs are used to being able to download a dependent open-source 3rd-party library to do anything non-trivial. Of course that doesn’t exist in C. But if you’re a dev worth your salt, then you just write your own shit when you need it, and it doesn’t take that long.
@@mensaswede4028 It only doesn't take that long if it's not much work in the first place. Sure, I would love to learn how to write a multicore async runtime, but I might as well save decades of my life by using one that has been heavily tested
Hi, glad you enjoyed my talk. Zig is actually developing systems meant to make build speeds very fast in debug mode which should solve the build-time issues you have. Another language worth looking into is Jai by game developer Jonathan Blow, tho the language is not yet public.
My impression is that many C programmers are borderline dogmatic about not using early returns, specifically because it makes the cleanup simpler without RAII.
Many are, yes. Some C style guides require it. Not everyone, though. In some circumstances, I find early returns dramatically clean up the code and avoid deeply nested if blocks.
There is a proposal to add PROPER defer to C. I'm not sure if a pure library implementation is possible. Google "A defer mechanism for C" (can't post links).
The dynamic array implementation has problems on architectures with strict data alignment. The dynarray_info struct might place your array elements in a misaligned address, which can degrade performance or crash on some architectures like ARM. This can be resolved by combining the array with the info using a flexible array member, so the compiler can ensure proper alignment.
Good catch. But how would flexible array member work with type safety? They have: "#define dynarray(T) T*" which allow you to use index operator directly on the "vector" but also without casts. I imagine your flexible array member would be of type unsigned char.
@@mettemafiamutter5384 I access elements through the flexible array member. Sort of like this (neglecting error & bounds checking for now): #define DYNARRAY(T) struct { dynarray_info info; T data[]; } #define DYN_PUSH(A) (A)->data[(A)->info.size++] size_t length = 100; DYNARRAY(long double) *numbers = malloc(sizeof(*numbers) + (sizeof(*numbers->data) * length)); numbers->info = (dynarray_info){ 0, length, sizeof(*numbers->data) }; DYN_PUSH(numbers) = 3.14159; Now sizeof(*numbers) == 32, with padding so data[] is properly aligned. I suppose you could also union dynarray_info with multiple max_align_t to achieve proper alignment. I'll have to play with that.
The defer macro described @ 18:00 hides a change in the semantics of the continue statement. Furthermore, the end will not execute if you return from the code block.
C took the /* ... */ comments from PL/I. Also, PL/I requires the parentheses for an expression being returned. The C statement syntax is just "return expression"... *but* you can always parenthesize an expression, so C will take "return x + y;" or "return (x + y);", and the people who developed Unix had previously worked on Multics, which was written in PL/I. C would thus be accepting of what someone coming from PL/I would do by force of habit.
What version of C? I'm trying to learn it thoroughly. I'm hearing that pointers are the way to go and that passing by reference is the preference ( I agree, passing by pointer / reference is the way to go) and yet on this video they avoid pointers. Hmm
I feel like passing by reference really depends on memory constraints and general size. On an arduino I would always pass by reference, but on my laptop it just depends. I always pass structs by reference but an int or smaller? Not really (unless i want to directly modify it). Just be sure to make it const so it can't be mutated if you have no intention of modifying the reference.@@Dazza_Doo
Go, Zig, Odin, C3 and others have prooved that defer is an awesome way to manage memory. Even though defer in this video is implemented as a macro, it was supposed to be part of the C23 standard . . . but it was delayed to a future version.
I've been programming in C since 1980, but I was not aware of the spectacular things you can do with struct and union initializers. This is an excellent video, thanks!
- 8:45 C++ has designated initializers now and yes you can initialize structs using "uniform initialization" as well. - 48:00 thankfully we have std::string_view now to avoid allocation hell.
@@alefratat4018 then use something like ETL (Embedded Template Library)? It only requires C++03 and implements its own alternatives to std::string and std::string_view
Thank you for putting this video together and sharing your knowledge. This is an extremely helpful knowledge share for someone like me just learning C.
Learned a lot, thanks for this! I would prefer to write my own game engine in C, but since I'm trying to get into the industry it seems I'm stuck with C++ since everyone uses it. 😢
I am started a game framework in C few weeks back. I have one .c file (I include .c files and not even bother with .h) for each module (App,physics2D,input, and renderer2D) and there basically its own library and they each have not dependencies on each other. Its a good and fresh break when I work on it from C++ Unreal.
C has remained my favorite language. It is very simple (i.e. not bloated) yet powerful and at the same time very pleasing to program (except string handling). Modern languages that try to be the new C/C++, like go, zig, and rust, don't capture the joyfullness of programing that C does and introduce a lot o friction and other quircks to use. I wish I could find a job using C.
"prefer value to pointer" wow that's kinda new to me, wouldn't it be expensive if the value is big? i usually would not pass by value if the value size exceeds a pointer size
Yeah, I'm also puzzled by that. I think of it like this until I find out how it really works: If it's reasonable to assume this struct can be stored in CPU registers, pass it by value. So my uneducated guess is you could safely pass around 9 values of 32 bit size, probably more. No idea what kind of hell breaks loose if you pass something really big, like contents of an image file by value. I expect nightmares.
It only really starts to become expensive if sizeof(type) > sizeof(void*) * 2 (and on some platforms up to 4). The C++ standard library does a lot of passing by value (when this is the case) and it's not known that it's slow.
@@grafgrantula6100 contents of something like image data will be stored in an array. And the array var will only contain the pointer(will decay to it when passed). So I don't see How is this even remotely possible. It may be possible if u use thousands of ints in a struct for doing that but why would anyone do that.
It depends. If you have a good code-structure than the compiler will see what you are doing and optimise accordingly. Say you are passing in a large struct by value, inside the function you are manipulating this local copy and then returning it, and at the calling-side you are not ever touching the original struct ever again: The compiler might very well re-use the old struct and never even create a copy.
My number 1 thing I wanted to be added to C is basically a copy and paste of Rust’s cargo system. Including libraries is the worst thing in C (Even in C++). I would also want float2, float3, float4, double2……… and so on added to C (Like HLSL). So that we all have 1 base to build off of. Everyone going around implementing there own is not very good for compatibility.
I am confused as to why the null terminator, simply one more character, should cause problems. It seems, rather, that problems arise from the tendency to allocate space on the heap for strings, make unnecessary copies, use pointers larger than size of the character type, and treating strings like variable length arrays. I would appreciate it if someone would explain why the null terminator causes problems.
I only use(ed) C for speed. When I see something like "Everything else gets set to 0" in struct initialization, I am thinking about more safe languages (scripting languages) that do things to protect you as a programmer and not trust the programmer with the data types. At least we know the C compiler will use the most efficient way to initialize all members to 0.... hopefully.
Scripting languages may protect your memory, but as soon as your project reaches a non-trivial size, you don't get much help with your types. The older I get, the more I appreciate static analysis. Maybe it's just incipient dementia, but the compiler does catch a lot of bugs for me these days...
The _Generic() example is not a good one. Try adding a string comparison function char *mins(char *a, char *b) and see how things break. A better option would be: #define min(a, b) _Generic((a), float: minf, int: mini, char *: mins)(a, b)
As for shared pointer issues in C I think I found a way to solve it with custom mutices, I attached octal permissions to it and made it so only the thread declared as the owner can destroy the attached data and only if the mutex has no threads attached and no threads with permission to attach (so when the owning thread tries to revoke it's own permissions it will only succeed if it is either passing ownership to another thread or the mentioned conditions are cleared, in which case destruction is triggered, the mutex pointer is no longer valid then)
modules, it is the reason why i gave up with C and C++ and moved to D i just don't want to deal with headers / predeclaration etc etc, this is such a pain that wastes me so much precious time
22:50 How does accessing floats in elements[2] work with struct padding involved; isn't it possible for padding to make the second float's offset more than the size of the first float? Having the elements array assumes that the floats are packed in memory, right?
Really nice talk, I especially like the error handling ideas. I'm no hardcore C programmer, but isn't avoiding passing by reference bad in the file load -> process -> pass to GPU example? The file data is copied when passed as a value to functions, so surely that's way less performant than using a pointer to the initial data, and passing that reference through the call pipeline? Or am I missing something? Does the compiler magically know what you're doing and switch to pointers before generating output?
in C if you dont use typedef like that you have to annotate the typename with the keyword struct everywhere, eg: struct foo {}; void bar(struct foo f); vs typedef struct foo {} foo; void bar(foo f);
Great talk! I really liked the solutions for defer and strings. But it was really painful that you don't mention DLang as you talk about these features in Go, Rust and others (except mentioning it at the end). DLang had these features when all these other languages didn't even exist.
I'd say use a microcomputer (the raspberry was the first) - one with easy access to an Assembler. You get to know the chip used (the architecture) and will never run into a problem with libraries as you understand what they rely on - Assembler. It's up and coming is Assembler. You run into problems with a x86 protected chip - which they all seem to be. Finally, we can make video games again!
What I dont get is why dont they make a new C version that breaks backwards compatibility on some code patterns and adds all these common patterns to the language itself
Developing in Code Blocks(2020) specifying the C99 standard to the compiler, it still forces struct to be written in the code:( In which development environment are these innovations implemented?
31:47 "vector" is a bad name for a dynamically sized array. I already have a lot of vector types in all my math libraries: - hardware vector types for efficient math on multiple values at once - linear algebra (vectors are just 1×n matrices) - geometric algebra (vectors are just 1D multi vectors)
So that difference of two sizes(some fn can do that for some stuff) doesn't under flow and become a large positive number instead of the expected negative or positive Maybe for negative indices like python has
I think it has something to do with undefined behavior, which weirdly enough would be a good thing in this case! I heard that using "int" instead of "unsigned" even when you always expect a positive value can lead to speedups thanks to compiler optimizations. Basically, for 'unsigned's, the compiler has to handle the possible overflow which can sometimes add a couple instructions; while for 'int's overflow is undefined so the compiler may act as if it could never happen (and provide no guarantee on the result)
often signed values are used to tell the compiler "hey, these values are small and we will pass along only sane values. So there is no reason for you to do any bounds/overflow-checking". But it can also be the other way around: since C99 integer division is rounded towards zero. This means dividing signed integers is slowed cause it needs to handle the negative case having a different rounding.
I got screwed by flash dying. Took me 8 years to figure out what to use next. I wanted something that wouldn't die on me when it takes me forever to build stuff. I chose C and JavaScript .
regarding vectors, it can do the trick, but what if you dynamically alocate at runtime? Defines are replaced at precompilation. Vector alocates on the heap, not on stack. To do the same job in c, it's quite a hassle..Also you have polymorphism in c++... Coming from embedded development using C, i see c++ as a much better language in some cases.
This idea of value oriented design is interesting. But for the embedded world on MCU, where C is still the main goto lang, it is not a good option where memory is so limited you dont want to pass cpy arguments. Btw thank you for this great presentation, I learn new ideas of API design notably on strings handling and error handling. I must admit I was a big user of goto err; syntaxe. Maybe I will re-consider this. Also I will dive into Zig, it looks promising... having error and build system built-in in the lang!
C's file I/O library has seemed extremely archaic. Whenever I tried to jump between reading and writing different files, it always messed up. In fact, I/O in general is something that needs to be fixed. It's pretty much the only reason why I'd prefer to develop in C++. When I call I/O functions, I don't want to have to clear the input or output buffer manually, because it's confusing. Not to mention that it never tells you when it's messed up. It's a very silent failure when you mess up I/O in C. Aside from that, the only major problems I've had have been with libraries that don't manage memory for you. I've been very quick to adjust to malloc() and free(), but SDL, for example, really forces you to read the documentation to find out if your program will leak memory. I've run Valgrind on my programs from a few months ago and found megabytes of unfreed memory, and none of it was from malloc(). Then again, I've had no idea about these special tricks you've put here. Generally, I manage memory and strings in C in a very simple way, which is "if I add a malloc() call, I add a free() at the exit points." I would explore that garbage-collection approach you've mentioned. It looks neat.
Very informative Thankyou What will be a good GUI library which works in MacOs & Windows & Linux for C Since you are a game developer you must be experienced & familiar with GUI libs (std applications development,Not Game) let us say something similar ‘look and feel’ Of Visual Studio Code IDE
You might even have your first_split function return a pair of string views, instead of mutating its argument. Not really sure if it's more convenient. typedef struct { str _0, _1; } str2; str2 first_split(str, str); str2 s = first_split(cstr("2021/03/12"), cstr("/")); assert(!memcmp(s._0.data, "2021", s._0.size)); assert(!memcmp(s._1.data, "03/12", s._1.size));
I always miss ASM and compilers. Should be mandatory in any language discussion. Remember, compiling isn't interpreting. I prefer approaching language from how these machines handle strings, like gnu readline.
It is kind of annoying that C++ is just partially allowing struct-initialisition now. And every time it is prossed it was shut down for stupid reasons that basically boiled down to "nobody needs this". Also C should start with giving us named parameters. that can be simulated with some macro-magic but really that should be part of the language. And C++ std::string_view - most of the time i would say stay clear of that. There are way too many pitfalls with that, not the least are the standard-library functions that might still convert it to const std::string&.
Great talk, very good presentation, but PLEASE don't code C like this. Pretty much none of the macros presented were safe macros, and I'm pretty sure I've seen a bug around. There's no need to use heavy macros like that, at this day an age compilers are sophisticated enough that using functions for this kind of stuff has zero overhead because it will auto inline them. In fact its like that by changing to functions with the use of 'restrict' you might even get a boost, because right now the compiler has no guarantees w.r.t. aliasing.
K&R C syntax has been in every standard from C89 through C11, but also declared deprecated the whole time. Compilers will not complain about an old-style parameter list, but may warn if any parameter type or the return type is implicitly int, for example when declaring the main function as "main(argc, argv) char **argv; { ... }".
I loved this video but, much of the things here, after getting a little familiar with rust, feels like rust has much easier way of doing all the things mentioned here. It feels rust is whole generations above C. I still enjoyed this video, C is an important language that paved much of the way to this modern software world, and it's nice to see, the C guys are still doing nice things, inventing nice ways to solve problems, betting better able to use the language, and stuffs.
"There is more to C than meets the eye"
Nice.
2:07 A refresh on C
4:04 Comments
4:18 Variables and structs
5:02 Primitive types
6:18 Functions
7:28 C++ is not C
8:11 Struct initialization
12:16 Struct initialization: sokol gfx
13:52 Modern C
14:54 C11 Additions
15:18 C11 Additions: static asserts
15:40 C11 Additions: _Generic and overloading
16:30 C11 Additions (cont)
17:06 Awesome macros
17:32 Awesome macros: defer
20:20 API Design: Math
21:18 API Design: Math in Modern C
23:18 API Design: Error handling
24:15 API Design: Error handling in Modern C
30:33 Generic APIs in C
31:40 Generic APIs in C: Dynamic Arrays
33:56 Generic APIs in C: Map
34:24 Libraries in C
37:06 When writing libraries
38:30 Memory Management
40:14 Temporary allocators
41:31 API Design: Modern C
42:15 String handling in C
42:54 Avoid libc
43:30 Replacing libc functionnality: printf
46:30 Case study: std::string
49:10 String handling in Modern C
51:50 String handling in Old C
53:40 String handling in Modern C (cont)
54:35 String handling in Modern C: another awesome macro (iterator)
55:41 First optimization at CA
58:40 Conclusion
Thanks for taking the time and creating timestamps for this talk, very useful.
This should be the first comment.
I write C every day as an embedded developer. It's funny to me that some people think C is some ancient obscure language. C is not going anywhere for a long time.
well, it is ancient
obscure? besides the variable declaration syntax, not really
although having a 7 year standard cycle is a bit too long imo
I love c
people should experiment with writing modular build systems for C. I would bet there are tons of different ways you could implement them.
@@kuhluhOG you do realize that us embedded developer write C99 when we're lucky, right? because most compilers are C90+extensions, some are C99+extensions. And the "slow" update cycle may be a sign that the language is mature and doesn't need new cool features/is stable/[insert another big advantage of C]
gcc for arm is probably C11 now but how many are using C11 features, really?
Exactly. I have been using C for the last 24 years. All my embedded work is in C. At Apple we used C exclusively for all the systems work including the OS, the Apple car and all its systems.
Great talk, Luca! I'm definitely looking forward to a v2.0 for C23! :)
I just finished developing a massive GStreamer plugin with ~10k lines of code. It's amazing how alive and exciting C programming is, and I'm a Gen-Z girl ;) While I work with other languages professionally, coding in C feels like an entertaining intellectual challenge. It also doesn't feel like you're doing something outdated, like when I had to translate some old Cobol scripts. No kidding, C is alive and long live C!
I noticed some great comments about tools like valgrind and the cflags sanitizer. I loved that Luca mentioned defer, I also sometimes use bdwgc (the garbage collector) and libcsptr (smart pointers) in the mix! Keep up the great work!
This is probably the best argument for C: Implement your own libraries.
The result: well defined, portable, benchmarked and optimized, *standard compliant* (ironically), source code.
This talk is great!, I was searching for a new language close to C, but this just reminded me the power of C. I love these ideas which keep the language updated.
Glad it was helpful!
The only real alternative to C/C++ is Rust.
@@NymezWoW or Zig
@@kuhluhOG or Jai
@@nephew_tom Jai is not available yet though ?
This dude and I are on the same wavelength. Well done!
one of the most useful talks on programming given in recent times
I like this. It's being like 20 years that I haven't touched serious c code. This inspires me to get more into the modern stuff
Ditto - I haven't touched C since the 80s and K&R 1. It was all pretty painful back then - even the tooling was a challenge. Modern C with modern tooling, such as the new VS interactive debugger, seems a much more attractive proposition.
People often argue that writing C code is quite dangerous because of buffer overflows and memory leaks, however there are tools which can help to detect the majority of those problems e.g. valgrind. I'll never go back writing a C program without valgrind.
Completely agree, and that is true for C++ as well.
As if there is any need to use unprotected manually managed buffers - there are so many good libraries out there and even just writing a simple wrapper-struct takes only a few minutes. You only use the lowest level stuff when you either have no clue or when you really need it (tight memory constraints, high performance, low latency - that kinda stuff).
-fsanitize=address with -lasan helps telling where in the code has corrupted stack
and debugging core dumps
if ( x != NULL) {}; There you go.
I'm currently writing a basic game engine for a graphical calculator in C and this talk couldn't have come at a better time ❤️
Good luck man!!
Good talk.
I too find modern c to be really refreshing, and use it for personal projects. Also great to see sokol-gfx, which is a joy to use.
The biggest problem I see with C is its lack of standard library.
It makes the language inaccessible to people for quick "getting to know the language"-type of projects.
Often the only alternative to using the standard library is either writing your own libraries or searching for other peoples libraries.
But without significant experience you just can't judge the libraries you find properly, and certainly can't construct a good one.
So beginners will use the standard-library, which is in many places crappy and very insufficient.
Especially string handling, which is also probably one of the first thing people want to do with a language.
Imo the top priority for any C "comittee" or people interested in pushing the language should be to band together and create a new defacto standard library based on modern c which everyone new then can simply use to get some stuff done.
THIS ^^^^^^^^^^^^
I agree, my biggest gripe with C is that I always feel like I have to implement the universe from scratch for any project I want to do.
I completely agree. C with a well designed and useful standard library would be amazing. It doesn't need to be huge, but it should have the basic building blocks such as strings, vectors, maps, etc. I don't like OO, but sometimes I use C++ just because I love the STL.
@@gaius_marius maybe glib is one option, not ideal but it's what we have for now
You don't need parentheses around a return value. It's return ; which can have parentheses around ,but return isn't a function. return(1)+2 returns the integer 3.
I hadn't seen this defer macro before. I like it.
c is still one of the best languages around. it simple, incredible powerrfull and the fastest language there is. i have prigrammed hava since ot was called oak and now a bit c# and alm the others but c is still king. if you want to write code for a nuclear powerplabt, space probe or the kernel for your phone, c is the labguage to use.
Fixing strings in C is actually hard, although representing strings as { data, size } makes a lot of sense, the problem is when it comes to interoperability with legacy APIs and external libraries, where zero-terminated strings are usually expected, in which case such a string view needs to be reallocated and appended with a zero just for the purpose of passing it to some external API. So it's a matter of preference, one can either have cheap string views / slices, or a natural (zero-terminated) string representation, but not both at the same time. So there's always some trade off.
Yes, good point!
Stupid question though: Cant we use a compatible_string that supports both null terminated via an extra pointer and the more handy {data, size} interface?
Yes, it complicates stuff and might not be worth it for all cases, but maybe for most of them?
@@grafgrantula6100 str is just storing that null terminated string in its data field
N calculating its length for the size
So for str u can just return
str->data to get back ur null terminated string
Hi, in regards to zero-terminated string API interoperability, just do what other langs do! You can have strategies where you copy the string to make it null terminated, or sometimes you can write your code such that you guarantee a string is null terminated. Also if you use a string_buffer rather than a string slice you can ensure the string_buffer always puts a null terminator at the end. A collection of strategies like this depending on the kind of program you write can certainly help and languages such as Zig are even better because the type system supports slices natively but also supports slices with sentinel values natively! So you can specify if a slice is meant to have a null terminator in the type system, pretty cool :D
You could have the underlying data just be a null-terminated string, represented by a { data, length } struct where the length is -1 to the underlying null-terminated string length. That way you get the more ergonomic interface where you have it, but you can also easily pass the string to legacy APIs and libs, as well as tap into the null-terminated C string *when you know what you are doing* by the convention that your string (or data pointer) is actually a null-terminated C string. All it costs is one convention and one extra byte per string, which is how C has it already anyway.
@@stianhoiland and that is basically what C++ has been doing with strings since C++11.
Even using Zig as a modern C compiler is quite nice. Cross-compilation and a unified build system you can write in Zig is a good bonus if you want to avoid the stack of tooling a typical C project needs.
what else do you need except for a compiler and a shell/batch script file?
@@ggss2836 Astyle, valgrind, a profiler, gdb, man pages ...
See where I'm going ?
@@ggss2836 a C compiler, a static analysis tool like valgrind, a debugger like gdb, a build system like make, cmake, meson, etc, a version control system very likely git, a way to track your dependencies like git submodules and/or your system's package manager
(at 43:19) math.h is not for libc, it's for libm ! And with tgmath.h, the math library is effectively generic (over long double, double, and float), which is *AWESOME*, and I think also part of modern C. But point taken that libc has issues.
He has eloquently voiced so many of my thoughts on C. The libc functions are RIDICULOUS. Also, strtok is pure comedy. Because of library design or build nightmares, C devs often have to re-implement very basic operations. If someone would design a sane, modern C std lib and a build system that could compete with cargo/npm/pip/etc... using C would not require any large tradeoffs from using other languages; and would have some very pronounced advantages, offering more control than any popular alternative. I did look into zig and rust; great languages, but those build times are not great. Idk if that's LLVM or just young compilers, but be prepared to get up and make yourself coffee every time you rename a variable.
Exactly, no one doing anything meaningful uses much of the standard C library. You have to be willing to build up your own more useful functions if you want to do anything non-trivial nowadays. But today’s culture is that of open-source dependencies where devs are used to being able to download a dependent open-source 3rd-party library to do anything non-trivial. Of course that doesn’t exist in C. But if you’re a dev worth your salt, then you just write your own shit when you need it, and it doesn’t take that long.
@@mensaswede4028 It only doesn't take that long if it's not much work in the first place. Sure, I would love to learn how to write a multicore async runtime, but I might as well save decades of my life by using one that has been heavily tested
Hi, glad you enjoyed my talk. Zig is actually developing systems meant to make build speeds very fast in debug mode which should solve the build-time issues you have. Another language worth looking into is Jai by game developer Jonathan Blow, tho the language is not yet public.
Please give V (vlang) a try!
Cargo for C would be so amazing.
defer macro is cool. However, unlike Go, it will not call the end function if you have an early return, break, or longjmp inside the block.
My impression is that many C programmers are borderline dogmatic about not using early returns, specifically because it makes the cleanup simpler without RAII.
Many are, yes. Some C style guides require it. Not everyone, though. In some circumstances, I find early returns dramatically clean up the code and avoid deeply nested if blocks.
There is a proposal to add PROPER defer to C. I'm not sure if a pure library implementation is possible. Google "A defer mechanism for C" (can't post links).
@@mettemafiamutter5384 That could be pretty nice. Thanks for the tip.
@@mettemafiamutter5384 to my knowledge C already has something similar, although it only accepts function pointers with void return and no parameters
The dynamic array implementation has problems on architectures with strict data alignment. The dynarray_info struct might place your array elements in a misaligned address, which can degrade performance or crash on some architectures like ARM. This can be resolved by combining the array with the info using a flexible array member, so the compiler can ensure proper alignment.
By flexible array member, you just mean a void pointer to the start of the array, rather than just assuming it follows after the info struct?
Good catch. But how would flexible array member work with type safety? They have: "#define dynarray(T) T*" which allow you to use index operator directly on the "vector" but also without casts. I imagine your flexible array member would be of type unsigned char.
In practice I doubt this will be a problem, i.e. sizeof(dynarray_info) will be a multiple of sizeof(max_align_t).
@@mettemafiamutter5384 not true on my architecture (actual code output, clang on Arm processor):
sizeof(dynarray_info) = 24
sizeof(max_align_t) = 32
@@mettemafiamutter5384 I access elements through the flexible array member. Sort of like this (neglecting error & bounds checking for now):
#define DYNARRAY(T) struct { dynarray_info info; T data[]; }
#define DYN_PUSH(A) (A)->data[(A)->info.size++]
size_t length = 100;
DYNARRAY(long double) *numbers = malloc(sizeof(*numbers) + (sizeof(*numbers->data) * length));
numbers->info = (dynarray_info){ 0, length, sizeof(*numbers->data) };
DYN_PUSH(numbers) = 3.14159;
Now sizeof(*numbers) == 32, with padding so data[] is properly aligned.
I suppose you could also union dynarray_info with multiple max_align_t to achieve proper alignment. I'll have to play with that.
As someone who is starting to learn C for the first time, this talk is very useful
The defer macro described @ 18:00 hides a change in the semantics of the continue statement. Furthermore, the end will not execute if you return from the code block.
C took the /* ... */ comments from PL/I. Also, PL/I requires the parentheses for an expression being returned. The C statement syntax is just "return expression"... *but* you can always parenthesize an expression, so C will take "return x + y;" or "return (x + y);", and the people who developed Unix had previously worked on Multics, which was written in PL/I. C would thus be accepting of what someone coming from PL/I would do by force of habit.
All I use is C. It’s the fastest portable and most explicit language out there.
What version of C?
I'm trying to learn it thoroughly. I'm hearing that pointers are the way to go and that passing by reference is the preference ( I agree, passing by pointer / reference is the way to go) and yet on this video they avoid pointers. Hmm
True, personally C99 is my fav
I feel like passing by reference really depends on memory constraints and general size. On an arduino I would always pass by reference, but on my laptop it just depends. I always pass structs by reference but an int or smaller? Not really (unless i want to directly modify it). Just be sure to make it const so it can't be mutated if you have no intention of modifying the reference.@@Dazza_Doo
Go, Zig, Odin, C3 and others have prooved that defer is an awesome way to manage memory. Even though defer in this video is implemented as a macro, it was supposed to be part of the C23 standard . . . but it was delayed to a future version.
I've been programming in C since 1980, but I was not aware of the spectacular things you can do with struct and union initializers. This is an excellent video, thanks!
Thank you for your comment which is much appreciated.
This is the most informative presentation on C I've come across to date. Well done
Thank you for commenting! Pleased to hear that this was a helpful presentation.
- 8:45 C++ has designated initializers now and yes you can initialize structs using "uniform initialization" as well.
- 48:00 thankfully we have std::string_view now to avoid allocation hell.
std::string_view only in C++17 though, still not supported by a lot of embedded systems toolchains.
@@alefratat4018 then use something like ETL (Embedded Template Library)? It only requires C++03 and implements its own alternatives to std::string and std::string_view
Thank you for putting this video together and sharing your knowledge. This is an extremely helpful knowledge share for someone like me just learning C.
Glad it was helpful!
Super Cool, thanks a lot!
Pretty nice talk! Thanks and we need more of this.
Thank you for your comment!
This is such an awesome talk
Great talk. Lots to unpack from this.
Learned a lot, thanks for this! I would prefer to write my own game engine in C, but since I'm trying to get into the industry it seems I'm stuck with C++ since everyone uses it. 😢
I am started a game framework in C few weeks back.
I have one .c file (I include .c files and not even bother with .h) for each module (App,physics2D,input, and renderer2D) and there basically its own library and they each have not dependencies on each other.
Its a good and fresh break when I work on it from C++ Unreal.
Lovely talk. defer and passing structs by value are really nice.
satisfying and sound assessment as well as interesting new features.
C has remained my favorite language. It is very simple (i.e. not bloated) yet powerful and at the same time very pleasing to program (except string handling). Modern languages that try to be the new C/C++, like go, zig, and rust, don't capture the joyfullness of programing that C does and introduce a lot o friction and other quircks to use. I wish I could find a job using C.
18:44 This `defer` is footgun™ as it does not call the end function on return statement.
or break or goto probably
Really interesting talk, thank you!
Thanks, this talk was of great help. I learned a few new tricks.
Great to hear!
Excellent talk. changed my view of the c language.
This is INSANELY good!!!
Great talk!
"prefer value to pointer" wow that's kinda new to me, wouldn't it be expensive if the value is big? i usually would not pass by value if the value size exceeds a pointer size
Yeah, I'm also puzzled by that. I think of it like this until I find out how it really works:
If it's reasonable to assume this struct can be stored in CPU registers, pass it by value.
So my uneducated guess is you could safely pass around 9 values of 32 bit size, probably more.
No idea what kind of hell breaks loose if you pass something really big, like contents of an image file by value. I expect nightmares.
It only really starts to become expensive if sizeof(type) > sizeof(void*) * 2 (and on some platforms up to 4).
The C++ standard library does a lot of passing by value (when this is the case) and it's not known that it's slow.
@Vironacorus For the aliasing issues, you can use restrict (but I get your point).
@@grafgrantula6100 contents of something like image data will be stored in an array. And the array var will only contain the pointer(will decay to it when passed).
So I don't see How is this even remotely possible.
It may be possible if u use thousands of ints in a struct for doing that but why would anyone do that.
It depends. If you have a good code-structure than the compiler will see what you are doing and optimise accordingly.
Say you are passing in a large struct by value, inside the function you are manipulating this local copy and then returning it, and at the calling-side you are not ever touching the original struct ever again: The compiler might very well re-use the old struct and never even create a copy.
Struct initialization has been part of c++ since c++14 i believe. Both in clang and gcc
They made it into the standard C++20. It might be possible that some compiler supported it before that.
I program EXCLUSIVELY in C, both front and back end.
My number 1 thing I wanted to be added to C is basically a copy and paste of Rust’s cargo system. Including libraries is the worst thing in C (Even in C++).
I would also want float2, float3, float4, double2……… and so on added to C (Like HLSL). So that we all have 1 base to build off of. Everyone going around implementing there own is not very good for compatibility.
I am confused as to why the null terminator, simply one more character, should cause problems. It seems, rather, that problems arise from the tendency to allocate space on the heap for strings, make unnecessary copies, use pointers larger than size of the character type, and treating strings like variable length arrays. I would appreciate it if someone would explain why the null terminator causes problems.
Nice talk. Love the defer macro
Me. I love C. Nothing better.
the presenters string library looks heavenly to work with lol!
Excellent talk!!! Thx a lot to Luca for sharing his knowledge with us ;)
Glad you liked it!
I only use(ed) C for speed. When I see something like "Everything else gets set to 0" in struct initialization, I am thinking about more safe languages (scripting languages) that do things to protect you as a programmer and not trust the programmer with the data types. At least we know the C compiler will use the most efficient way to initialize all members to 0.... hopefully.
Scripting languages may protect your memory, but as soon as your project reaches a non-trivial size, you don't get much help with your types. The older I get, the more I appreciate static analysis. Maybe it's just incipient dementia, but the compiler does catch a lot of bugs for me these days...
amazing talk!!!
The _Generic() example is not a good one. Try adding a string comparison function char *mins(char *a, char *b) and see how things break. A better option would be:
#define min(a, b) _Generic((a), float: minf, int: mini, char *: mins)(a, b)
Such an information video. I just wanted to know that from where did you learn these C language techniques. Is there any book available?
As for shared pointer issues in C I think I found a way to solve it with custom mutices, I attached octal permissions to it and made it so only the thread declared as the owner can destroy the attached data and only if the mutex has no threads attached and no threads with permission to attach (so when the owning thread tries to revoke it's own permissions it will only succeed if it is either passing ownership to another thread or the mentioned conditions are cleared, in which case destruction is triggered, the mutex pointer is no longer valid then)
That "defer" macro was very cool 😎
modules, it is the reason why i gave up with C and C++ and moved to D
i just don't want to deal with headers / predeclaration etc etc, this is such a pain that wastes me so much precious time
i want some sort of modules in modern C, otherwise it'll always feel bad..
@@Hoowwwww there is a similar project by Jens Guesdt(he's on the ISO C board, co author of C11 standard), just search it.
22:50
How does accessing floats in elements[2] work with struct padding involved; isn't it possible for padding to make the second float's offset more than the size of the first float? Having the elements array assumes that the floats are packed in memory, right?
right, padding would break it, but are there platforms which pad for an 8byte alignment?
Really nice talk, I especially like the error handling ideas. I'm no hardcore C programmer, but isn't avoiding passing by reference bad in the file load -> process -> pass to GPU example? The file data is copied when passed as a value to functions, so surely that's way less performant than using a pointer to the initial data, and passing that reference through the call pipeline? Or am I missing something? Does the compiler magically know what you're doing and switch to pointers before generating output?
the struct itself doesnt contain the file data it just contains a pointer to the file data
Great talk! I wonder why you declare structure types as:
typedef struct kv {...} kv;
instead of a simple:
struct kv {...};
in C if you dont use typedef like that you have to annotate the typename with the keyword struct everywhere, eg:
struct foo {};
void bar(struct foo f);
vs
typedef struct foo {} foo;
void bar(foo f);
So you don’t have to type out struct every time
It allows you to not have to write "struct kv" everywhere
so that we don't have to write struct every time we make a variable or pointer for struct kv, like kv *newvar; instad of struct kv *newvar;
Hello, just struct kv {...}; won't compile in C :(
excellent talk thanks
47:05: It most likely doesn't allocate due to small buffer optimization
Good stuff
Glad you enjoyed
Great talk! I really liked the solutions for defer and strings. But it was really painful that you don't mention DLang as you talk about these features in Go, Rust and others (except mentioning it at the end). DLang had these features when all these other languages didn't even exist.
Great point!
Good point, I plan on giving a refresh on this talk soon and I will definitely mention Dlang too!
So good uwu
Note: Rust's error handling is also very built-in, the try keyword of Zig is the ? unary operator in Rust ^^
I'd say use a microcomputer (the raspberry was the first) - one with easy access to an Assembler. You get to know the chip used (the architecture) and will never run into a problem with libraries as you understand what they rely on - Assembler. It's up and coming is Assembler. You run into problems with a x86 protected chip - which they all seem to be. Finally, we can make video games again!
What I dont get is why dont they make a new C version that breaks backwards compatibility on some code patterns and adds all these common patterns to the language itself
you already answered the question yourself
Developing in Code Blocks(2020) specifying the C99 standard to the compiler, it still forces struct to be written in the code:(
In which development environment are these innovations implemented?
31:47 "vector" is a bad name for a dynamically sized array.
I already have a lot of vector types in all my math libraries:
- hardware vector types for efficient math on multiple values at once
- linear algebra (vectors are just 1×n matrices)
- geometric algebra (vectors are just 1D multi vectors)
At 44:16 shouldn't `log("Cat: {cat}", c);` be `log("Cat: {cat}", &c);` ?
No u just pass c by value
Then the log function
Passes &c to the registered function(print cat, probably stored in a hashmap as type function pair)
Any reason why isize_t is used for things that should be unsigned (size_t)?
I have the same question
So that difference of two sizes(some fn can do that for some stuff) doesn't under flow and become a large positive number instead of the expected negative or positive
Maybe for negative indices like python has
I think it has something to do with undefined behavior, which weirdly enough would be a good thing in this case!
I heard that using "int" instead of "unsigned" even when you always expect a positive value can lead to speedups thanks to compiler optimizations. Basically, for 'unsigned's, the compiler has to handle the possible overflow which can sometimes add a couple instructions; while for 'int's overflow is undefined so the compiler may act as if it could never happen (and provide no guarantee on the result)
often signed values are used to tell the compiler "hey, these values are small and we will pass along only sane values. So there is no reason for you to do any bounds/overflow-checking".
But it can also be the other way around: since C99 integer division is rounded towards zero. This means dividing signed integers is slowed cause it needs to handle the negative case having a different rounding.
I got screwed by flash dying. Took me 8 years to figure out what to use next. I wanted something that wouldn't die on me when it takes me forever to build stuff. I chose C and JavaScript .
regarding vectors, it can do the trick, but what if you dynamically alocate at runtime? Defines are replaced at precompilation. Vector alocates on the heap, not on stack. To do the same job in c, it's quite a hassle..Also you have polymorphism in c++... Coming from embedded development using C, i see c++ as a much better language in some cases.
Honestly, polymorphism is not that great. Sure, it makes the code 'prettier' but also harder to follow or debug, IMHO.
Very nice!
Thanks!
This idea of value oriented design is interesting. But for the embedded world on MCU, where C is still the main goto lang, it is not a good option where memory is so limited you dont want to pass cpy arguments.
Btw thank you for this great presentation, I learn new ideas of API design notably on strings handling and error handling. I must admit I was a big user of goto err; syntaxe. Maybe I will re-consider this.
Also I will dive into Zig, it looks promising... having error and build system built-in in the lang!
The value oriented design works well for the str (pointer+size pair), but not the holding buffer.
few people get it C is infinite in scope you can do anything you want with it and in any shape you want the only limit is yourself
C's file I/O library has seemed extremely archaic. Whenever I tried to jump between reading and writing different files, it always messed up.
In fact, I/O in general is something that needs to be fixed. It's pretty much the only reason why I'd prefer to develop in C++. When I call I/O functions, I don't want to have to clear the input or output buffer manually, because it's confusing. Not to mention that it never tells you when it's messed up. It's a very silent failure when you mess up I/O in C.
Aside from that, the only major problems I've had have been with libraries that don't manage memory for you. I've been very quick to adjust to malloc() and free(), but SDL, for example, really forces you to read the documentation to find out if your program will leak memory. I've run Valgrind on my programs from a few months ago and found megabytes of unfreed memory, and none of it was from malloc().
Then again, I've had no idea about these special tricks you've put here. Generally, I manage memory and strings in C in a very simple way, which is "if I add a malloc() call, I add a free() at the exit points." I would explore that garbage-collection approach you've mentioned. It looks neat.
Where were your memory leaks ?
I use SDL2
For IO just use your operating system's API. In fact I would prefer to use it for memory management too. It's faster and gives you better control
Very informative Thankyou
What will be a good GUI library which works in MacOs & Windows & Linux for C
Since you are a game developer you must be experienced & familiar with GUI libs (std applications development,Not Game) let us say something similar ‘look and feel’ Of Visual Studio Code IDE
9:00 wrong, you CAN do it in C++. Search for "Designated initializers" on the "Aggregate initialization" page of cppreference.
27:35 I hate adding prefixes and suffixes to specific names. Like "_t" to types in this case.
You might even have your first_split function return a pair of string views, instead of mutating its argument. Not really sure if it's more convenient.
typedef struct { str _0, _1; } str2;
str2 first_split(str, str);
str2 s = first_split(cstr("2021/03/12"), cstr("/"));
assert(!memcmp(s._0.data, "2021", s._0.size));
assert(!memcmp(s._1.data, "03/12", s._1.size));
nice
Just the function overloading using _Generic is not that stable as in c++
I always miss ASM and compilers. Should be mandatory in any language discussion. Remember, compiling isn't interpreting. I prefer approaching language from how these machines handle strings, like gnu readline.
It is kind of annoying that C++ is just partially allowing struct-initialisition now. And every time it is prossed it was shut down for stupid reasons that basically boiled down to "nobody needs this".
Also C should start with giving us named parameters. that can be simulated with some macro-magic but really that should be part of the language.
And C++ std::string_view - most of the time i would say stay clear of that. There are way too many pitfalls with that, not the least are the standard-library functions that might still convert it to const std::string&.
Great talk, very good presentation, but PLEASE don't code C like this.
Pretty much none of the macros presented were safe macros, and I'm pretty sure I've seen a bug around. There's no need to use heavy macros like that, at this day an age compilers are sophisticated enough that using functions for this kind of stuff has zero overhead because it will auto inline them.
In fact its like that by changing to functions with the use of 'restrict' you might even get a boost, because right now the compiler has no guarantees w.r.t. aliasing.
Where are your benchmarks and unit tests @:)
I did not understand how the defer macro works 😞
Was the old function syntax even in the first standard C90
K&R C syntax has been in every standard from C89 through C11, but also declared deprecated the whole time.
Compilers will not complain about an old-style parameter list, but may warn if any parameter type or the return type is implicitly int, for example when declaring the main function as "main(argc, argv) char **argv; { ... }".
C language is my mother tongue 👅
You forgot to mention Scopes here.
The thing is not the language but the purpose
Does anyone have any further reading I can do on modern C? I'm very interested
C is great. I want a gui framework.
I have to write my own ...
there's gtk and blendish+nanovg.
don't forget iup
26:10 Isn't this what monads are?
its something you can do with monads, but monads are a strictly more general concept.
I loved this video but, much of the things here, after getting a little familiar with rust, feels like rust has much easier way of doing all the things mentioned here. It feels rust is whole generations above C.
I still enjoyed this video, C is an important language that paved much of the way to this modern software world, and it's nice to see, the C guys are still doing nice things, inventing nice ways to solve problems, betting better able to use the language, and stuffs.