C++, D, Rust, ...
> Want to trivially import any of the huge array of existing libraries? You guessed it...
If C can talk to it, so can anything else. Granted, having the preprocessor means no bindings, but C++ has that too.
> Want to do something you really shouldn't, and any 'sensible' language might attempt to stop, but is called for right here and now...
C++, D.
Want undefined behaviour? C. Want segfaults? C. Want memory leaks? C. Want memory corruption? C. Want to write insecure code? C. Want to write your own containers? C. Want Go's approach to generics? C. Want to spend 3x as much time writing code as a comparable C++ solution (not to mention more productive languages)? C.
I've known C for 21 years now. I just quit a job as a C programmer for embedded systems. If I can help it, never again.
Want all of that and a whole bag of inconsistent design choices? C++. Because, yes, one day you will meet a C++ codebase which is a mixture of "forbidden C++" and "modern C++".
Perhaps so, but reading it back takes 5 times as long.
Coming from Python and dabbling in C for embedded software, this is the one that gets me the most. It's so frustrating to be spending hours doing something you can do with one line of the regex library in Python...
Overall though I think what you may be seeing is simply the stratification of software development; what began in the 70s and 80s has come to fruition. There are different planes of existence, like thermoclines in the oceans. A developer may (and is likely to) live in one forever, without crossing above or below. Each has its purpose. While I wrote my own database and rendering engines in the 80s as part of applications development, I wouldn't dream of it today unless there was a very, very good reason. Same goes for network stacks. Or math libraries. Memory managers. Etc.
Sure, coming from the Python ecosystem, you will feel as though there is a shortage of auto-pilot-like assistance, but you must realize you are in a very different world now - Embedded work is (frequently) focused on predictability, responsiveness, efficiency, and even moreso with realtime work. You are the engineer in charge, and all the safe boundaries and cushions of the interpreted language world are now missing. Want to REALLY feel naked? Drop down to assembly on any platform and try to emit a simple "hello world." For those who have never done so, it's a humbling experience but rewarding.
Remember that C was designed as a "universal assembly language", easily portable to different architectures and close (enough) to the metal so the programmer can have full control over the machine. Best analogy I've heard: C is like a chainsaw - used carelessly one can cause enormous damage and destruction, but in the hands of a skilled/experienced craftsman, very clever solutions are possible.
Mostly because I'm used to one single place for libraries (PyPI) and I don't know how to find (and evaluate) libraries in C. I also don't know if they'll run on my MCU or how much RAM they require. I have found one or two embedded regex libraries, but they were very limited in what they could do or failed some other criterion.
Besides, that was just an example, it's just death by a thousand cuts with similar things one after the other.
you can replace C with any language. you're evangelising about the toolbox you know instead of learning about the hammer you are familiar with and don't want to learn how to operate
You probably wouldn't do that in Python; the regular expressions module is part of the language.
The Lua docs provide some examples of integrating Lua into C here - https://www.lua.org/pil/24.1.html.
If your target only has two KB of RAM, Lua is a no-go, I guess. But skill-wise, I can highly recommend it. The reference manual is very well-written, the Lua language itself is very nice and easy to learn, and Lua's C interface is very pleasant to use; and again: the documentation is very good. (Also, the community is very friendly!)
If you ever take a shot at it, the Lua Users Wiki (http://lua-users.org/wiki/) is a good starting point.
However it doesn't give you the control or the performance of C without some serious work, some of which is usually work in C!
On top of the additional testing, you also will have a performance problem at some point after success. And then people will discuss or even plan for a rewrite in a more performant, language. But we all know that rewrites usually fail to materialize. PoC's turn into products, products turn into companies, and companies bottom lines can't generally spare time and money to rewrite the entire service.
I'm not advocating for C, but I wouldn't put Python up as a comparable thing.
C code that compiles can and does blow up at runtime.
There are languages whose type systems do go a long way toward minimizing the risk of this happening, but C is not really one of them.
I personally wouldn't choose either language for a new project.
As a noob C programmer, in my programs segfaults often occur along with undefined behaviour (which is much worse, as your program will silently give the wrong output).
While in Python, you'd get a traceback which is much easier to debug.
> Requiring a massive set of tests to actually validate your code.
Both require tests to validate your code. Sure, static typing can verify some trivial mistakes, but they can't check that array accesses are in bounds, integer overflow, etc.
> While in Python, you'd get a traceback which is much easier to debug.
Well, C's segfault will also generate core dump, which is essentially the same thing as Python's traceback - it tells you where the program crashed.
C is not typesafe either, and blows up for other reasons at runtime.
Which everybody here knows and nobody doubted. So, a big duh!
What the parent means is with Python you don't need to go and install one, adjust your makefiles, etc. Regex are right there in the standard library, and things that are not are immediately usable in just a "pip install" away.
Do people actually use pip when you can just use the package manager of your distro?
The conversations that occur here generally seem much more structured.
Engineering is a life and death business more often than people think; it deserves people who feel strongly about it.
As an aside, I think the rules about trolling and personal attacks are pretty clear and easy to enforce. What I'm specifically concerned about is that those rules are evoked outside of the spirit in which they were created to attack emphasis in general. In fact, most of the discussion here is in clear violation of "Please avoid introducing classic flamewar topics unless you have something genuinely new to say about them." but obviously we rightly care a lot about this topic and, since people are acting like adults, no one needs to tap on the poster so to speak.
I appreciate that the majority of people in this forum express their opinions in a highly productive manner void of animosity, as you've just demonstrated. The previous's user's remark "Worst argument so far." did not convey this spirit. Whether intentionally or not, antipathy and pure negativity do not bring out the best of people in online forums. It is my hope that those are two things which this community seeks to discourage, but not all freedom of expression. This is how I interpreted the comment which sparked your initial response.
(I know there are some "tricks" you can use to make smaller Rust binaries by chopping things out, but of course the same applies to C. For example, I can take the C binary and just truncate the file down to 2230 bytes without breaking it)
C binaries are small because they're assuming you've installed what is ultimately the C runtime on your system (libc).
The tricks you describe are just changing the compile options to those that C uses by default.
on top of that you can apply the same tricks as C; e.g. by running strip, truncating the binary, writing a linker script, etc.
For more: https://lifthrasiir.github.io/rustlog/why-is-a-rust-executab...
Or if being verbose is desired, do you want to do a job for a computer that you are not suited to do but the computer is?
I want optimal syntactic sugar.
Like Rust or C++ templates? I prefer C syntax over the "optimal" alternative.
Can be solved by not doing stuff that are considered as undefined. Think of them as the standard's say of saying "don't do that". It's quite easy to do in most cases.
> Want segfaults? C > Want memory corruption? C > Want to write insecure code? C
There is absolutely nothing in C that makes it more prone to these in comparison to other languages. Moreover you can easily solve these by using an implementation that protects you. (such as one of the multiple C compilers that guarantee to produce safe code without UB or the sanitisers in gcc and clang)
> Want memory leaks? C
Can be easily avoided by avoiding dynamic memory allocation. Moreover nothing stops an implementation from using a GC.
> Want to write your own containers? C
This applies to all the languages as none of them happen to have an implementation of every container in their standard library. It is true that C lacks that in comparison to some other languages, however implementing the common ones (or using a library) is an extremely trivial thing to do.
> Want to spend 3x as much time writing code as a comparable C++ solution (not to mention more productive languages)? C
You must be quite the shitty C programmer to need 3 times less time to write something in C++.
I have a list of "don't do that" rules. I still do them, usually by accident.
> There is absolutely nothing in C that makes it more prone to these in comparison to other languages
If you believe that, then there's nothing I can say that will change your mind.
> Moreover nothing stops an implementation from using a GC
Many applications can't use a GC. For tiny systems, forget it.
> none of them happen to have an implementation of every container in their standard library
That completely sidesteps the fact that in pretty much every other language the containers one would like to use 99% of the time are implemented in the standard library or the language itself.
> You must be quite the shitty C programmer to need 3 times less time to write something in C++.
Only a shitty C++ programmer _doesn't_ write code 3 times as fast as they'd write C.
Throw in the level of control with sprintf() and there's a pretty clear win.
What is easier with C++ ( and most scripting languages ) are things like combinators and regexp.
As is *cat being O(N) with respect to destination length.
So easy that there's been constant flamewars over nonstandard alternatives such as strlcpy, strcpy_s... https://news.ycombinator.com/item?id=6940368
I've worked in numerous languages and platforms, even changed careers from programming (graphics) to film and television. But I'll always be a C programmer.
However, for me, C falls down on large projects. I know there a lot of hate on objects lately, but the complexity of a project decreases while the maintainability increases when you use properly designed interfaces. You “force” the new guy to do it right because the interface makes it obvious. I know there are ways to enforce encapsulation with C, but I’ve never worked on a (large) codebase where this has been successful.
I try to organize into specific components and piece them together into a sequential or threaded or both model, depending on what I do. Of course, it is specific to what I do. But there are large C projects that are successful - for example, Linux kernel.
C++11 really does put that to rest. Move semantics basically enable complete avoidance of manual memory management. There are many other powerful and convenient aspects, but in C++11 and on managing memory essentially goes away. I essentially never allocate memory explicitly.
Depends on what you do, I guess. I process huge image sequences and have them run (play at least) in real time. I cannot afford to not allocate memory manually. In fact, I have to and I have to manage heap myself. I also have to manage thread affinity, in order to have threads (producer/consumer) share L2 cache to get the speed I want, and all that on multiple processors.
Sometimes you must do that manually.
You say that, but I deal with almost the exact same things. What I mean by manual is something like malloc where the freeing of memory is manual. Sean Parent would call this 'no raw pointers'.
For starters, the fact that memory allocation is even an issue seems odd to me, I'm not sure why you wouldn't allocate large chunks of memory rarely.
Even if there are OS specific calls I'm making to deal with special memory, I make sure it is wrapped with some simple class or struct that has a destructor and likely a move constructor. Alignment issues can be wrapped as well, then there is no doubt who owns the memory, who frees it, and where/when it is freed. I've never seen a situation in which raw malloc/free are necessary or any control is lost using C++.
I doubt you suddenly want to use C++ still, I'm willing to bet you know C extremely well, but at least realize that your choices are not based off of technical reasons.
One thing is sure though. You're right most of reasons aren't technical. I'm very confident about what I know and what I don't know in C and I don't have to second-guess (usually) what I want to do, because my experience lies with C. It's a subjective choice, but also a technical - since that knowledge and experience is my technical asset. It's not like you can't do something in C that you can't do in C++. In fact, you can do it almost the same if you want to, but with C++ you would be a better fit if you follow C++ idioms, which I haven't picked up over the years (especially after STL cambrian explosion) and something I think I won't in the near future. Reason is simple. Everything I do, I do mostly alone and so far C has been good to me. I venture into other things, out of curiosity and sometimes necessity, but I always kind of fall back to C. Sometimes it's due to the fact that I know of top of my head how to do something quick (which I often need to) without resorting to googling or I don't know how to make it run quick enough since I'm not intimate with the language (not C++ in this case, but in general).
You know what you get with C and it just works as you expect always. I always feel refreshed after writing C.
There are perhaps a few very minor enhancements I'd suggest, but I'd be very reluctant to open the floodgates and ruin it.
C is one of the few languages, along with C++, that revels in undefined behaviour. It can be very hard to reliably know what a C program will do if it's not written very carefully because there are so many constructs that look benign but which are technically wrong, and the compiler will mercilessly exploit in order to optimise your program into nonsense.
Actually - not completely true. I do like to look at the assembly from time to time for other reasons, but this is rare.
1) Diagnosing a crash which turned out to be the result of a 1 byte vtable pointer corruption bug in an older codebase, which turned out to be a bad static_cast in relatively removed code (a good case for boost::polymorphic_downcast!). Simply understanding which pointer was bad in the first place required looking at the disassembly - when you can't rely on your debugger's results thanks to optimization.
2) Figuring out the actual values of variables in crashes and crash dumps of optimized builds to properly root cause a bug, when the debugger gets confused - or simply aggressively inlined and reordered everything so aggressively that there's no sensible values to even display (so, most crash dumps.)
3) Noticing when the optimizer has reordered code "unexpectedly", alerting me to the fact that supposedly thread safe code is in fact nowhere nearly remotely safe and is in fact missing many memory barriers (possibly because their portable macros "helpfully" defaulted to a noop on whatever new and previously unrecognized platform I'm porting to.)
4) Noticing when the optimizer has removed or rewritten code in an "incorrect" manner, helping me debug code that would've worked if it hadn't technically invoked undefined behavior, so I can a) fix it, b) attempt to explain to my coworker that, yes, it's really undefined behavior, and yes, it's actually a problem (typically with a combination of citing the standard and linking INVALID WONTFIX-ed "bugs" in some compiler's bug database), c) be reasonably certain I've actually found the real root cause of a bug and fixed it.
Now, yes, I'll admit this isn't 100% of my debugging sessions. And perhaps I'm an outlier. My coworkers generally learn that I can (eventually) tackle pretty much any weird bug they might be struggling with and that I'm happy to help. All the porting I've done reopens a whole codebase's worth of wounds - latent undefined behavior that another compiler's optimizer didn't take advantage of.
But on the other hand, I've been lucky enough to never encounter a codegen bug in all the compiler and linker bugs I've found. So far. That I know of. And while "rare" by incidence, these are the debugging sessions that can eat weeks at a time for a single bug, when sufficiently nasty and novel.
Well, it works as defined in standards. It's just not every programmer knows what to expect. C is simple, yet powerful language and with powers comes responsibility. It's not like you can throw some libraries/modules/objects (or whatever it is in other, safe languages) together, upload to server and call it a day -- static testing, debugging, unit testing is a vital part of any semi serious C project.
You think so? Every company I've worked for, or that I've known people that worked there, always enabled -Wall for their C and C++ code. Most OSS software compiles with all warnings enabled.
I think the issue with undefined behavior in C/C++ is extremely overblown, aside from fun academic examples like 'what does i++++i++ evaluate to' there isn't actually all that much undefined behavior or gotchas in C/C++. I would say there are less, compared to other languages I know.
You need to know that the problem exists in order to know that you have a problem. There are many C programmers who learned C back in the 1980s who don't even realize these are issues.
I'm still adding the compiler specific annotations to add format string checking to custom variadic logging functions in codebases I inherit, and finding multiple bugs.
Of course you want -Wall -Wextra -Werror -pedantic. ;)
There's nothing like the experience of trying to fix somebody else's code which compiled fine on gcc version 8.97 but which now fails to compile on gcc version 8.98 because the new compiler has some new warnings, which it's now treating as errors, and now fails to compile.
...and you've got stuff to do, and the program isn't even broken.
Well, it may be — that's one of the problems with C: you never really know for sure if a warning really matters or not. But man, there sure are a lot of them!
Last I checked he has a couple hundred points on the hacker news internet forums.
Also just last week I found and reported some undefined behavior in a major c++ package that's used by almost every player in as many as several industries. I don't expect it will ever make any difference in production, but it still snuck in.
I'm sure you could say that about pretty much any programming language: "The amount of terrible X in the real world is enormous". There are also plenty of clean, nice, safe C code around (and any other language), there's no need to over-generalize ("Almost no one else does").
But the damage is far greater in C. In other languages you won't have arbitrary code execution or privilege escalation just because the programmer is not careful. Nor will there be, in other languages, so many nondeterministic bugs that show up once in a blue moon.
Sure you do. Remember the YAML fiasco with Ruby? How about the thousand-and-one RCE issues with PHP? eval isn't evil for no reason.
No, it's possible to make system insecure with pretty much any language if programmer is not careful. SQL injection, cross-site scripting, cross-site request forgery and the list goes on..
There's bad code everywhere. Some languages make it a bit easier, but it's really not the languages fault.
If your best defense of a language is "well, at least people use it", that's a bad sign.
void func(void *ptr)
{
uint32_t *ip = ptr;
ptr[0] = 123;
}Otherwise most every assignment after call to malloc would be undefined.
From C1X, section 6.3.2.3:
"A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined."
Though that is quite odd, since any pointer can be converted to void* , which only needs alignment to the char type. So converting from x* -> y* is undefined, but x* -> void* -> y* is defined.
>> uint8_t x[100];
>> uint32_t *y = &x[1];
And then dereference y, most RISC architectures will trap on the unaligned access. It doesn't matter if there is an intermediate void pointer or not.
According to my reading an intermediate void pointer allows the pointer casting to stay well defined. However this seems unsafe, even without getting into dereferencing, because implementations are allowed to store omit bits if they assume aligned pointers.
"A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined."
The resulting uint32_t pointer in my example is not correctly aligned for the reference type, so undefined behavior (e.g., a trap on RISC) occurs. What's an example of a statement in a "non-trivial" C program that is in common use but you think is undefined?
(1) I didn't say your example didn't demonstrate a violation, but it misses the point, because it doesn't invoke an intermediate void pointer:
"A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer."
(2) That was my attempt at coming up with a good example, but it seems, due to the above clause, the casting between incompatible pointers via void * is technically "legal".
Undefined:
int64_t a = 42;
void* p = &a;
int32_t* i = p;
printf("%i", *i);
Implementation defined, as type punning to char is legal (allowing the implementation of memcpy): int64_t a = 42;
void* p = &a;
char* ch = p;
printf("%c", *ch);
Exercise left to the reader: Implement a "fast" memcpy (e.g. one that will copy more than 1 byte at a time for large copies, as your standard library implementation likely does) without violating strict aliasing rules. An object shall have its stored value accessed only by an lvalue expression that has one of the following types 73) or 88):
* a type compatible with the effective type of the object,
* a qualified version of a type compatible with the effective type of the object,
* a type that is the signed or unsigned type corresponding to the effective type of the object,
* a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
* an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
* a character type.
73) or 88) The intent of this list is to specify those circumstances in which an object may or may not be aliased.
Bullet 6 is what allows the second sample to have defined behavior. For the first sample, unless I'm seriously mistaken, int32_t isn't considered "a type compatible with" int64_t. Bullet 2 talks of "qualified" versions of types - I believe this is referencing const/volatile qualified types. Bullet 3 apparently allows you to type pun (unsigned int) to (signed int) or vicea versa? Which is an interesting bit of new trivia to me. Bullet 4 is much of the same, bullet 5 requires a nonexistant union, and bullet 6 requests a character type.I still wonder if my snippet counts as undefined behavior, since it does dereference an "unknown" void pointer, which may have come from an incompatible object type.
BTW, the latest C1X draft is only at http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
It counts as potentially undefined behavior - depends on what you pass in. NULL? UB. Pointer-to-uint64_t? UB. Pointer-to-uint32_t? Perfectly defined behavior! ...well, assuming we use ip[0] = 123; instead of ptr[0] = 123;, which won't compile as I've just noticed.
That said, there are some ways to construct pointers which are in and of themselves undefined behavior for merely constructing the pointer:
http://stackoverflow.com/questions/23683029/is-gccs-option-o...
More samples:
int a[] = { 1, 2, 3 };
int* b = a+0; // Perfectly defined/legal/normal
int* c = a+3; // Perfectly defined/legal/normal, just don't deference it (as it points past the end of the array)
int* d = a+4; // Undefined behavior. HAIL SATAN!
int* e = a-1; // Undefined behavior. Also apparently potentially caused optimization induced breakage in practice. HAIL GCC!It's the complete opposite of waiting for a feature to appear in the compiler.
Every language works just as you expect if you have the right expectations.
"C is memory with syntactic sugar." Dennis Kubes
So... you compile with optimisation switched off?
For instance, when writing a scalar-processing loop over a fixed length array with no dependency between iterations you can reasonably expect the compiler to convert to SIMD instructions and do some unrolling. It's not too difficult to check the binary that this happened, and if it doesn't happen you just write it yourself with intrinsics.
I often think of C + compiler's optimization passes as a "scripting language for assembly".
while (*++targ = *++src)
;Unless the data is user-generated, in which case you might have a security issue on your hands... :)
Kinda like with everything else related to C!
The problem is, it's also up to the programmers who worked on my codebase before I did. I would never write that atrocity, but I've come across that atrocity quite a few times in code I needed to modify.
(but usually generates compiler warning without an additional set of parentheses)
Want minimal syntactic sugar and a language that's not prone to feature-of-the-week hipsterism? C
Want to make tiny binaries for some obscure system with virtually no storage or RAM? C.
Want to know where every last cycle goes and explicitly control everything? C
Want to trivially import any of the huge array of existing libraries? You guessed it...
Want to do something you really shouldn't, and any 'sensible' language might attempt to stop, but is called for right here and now...
It's not the safest language, it's not the most expressive, but the syntax is small enough to hold in your head and the body of existing, available work is HUGE. I've been working in C for 15 years and, while I enjoy forays into other languages, it will always be my love.