That's kind of my opinion. C didn't die for legacy reasons, but starting new project in C seems a nonsense to me.
C++ just has so many features that makes the development of programs and libraries easier, for almost no extra costs.
That's kind of my opinion. C didn't die for legacy reasons, but starting new project in C seems a nonsense to me.
C++ just has so many features that makes the development of programs and libraries easier, for almost no extra costs.
I respect your opinion, but mine is the exact opposite (I don't know how representative mine is, but I'm sure I'm not alone).
The problem with C++ is that many of those "features" are actually "anti-features". From the top of my head: operator overloading, copy-constructors, constructors, destructors, function overloading. While these "features" may make correct code easier to write, they always make incorrect code much, much harder to understand.
For example, you find a simple line of C++ that says "y=f(x);", and it can do anything: has the parenthesis operator been overloaded? and the copy constructor? how many times is a destructor called here? What are its side effects? What instance of f is called, if none of them is defined for the actual type of x? Every single line of C++ that you have to analyze is a terrifying nightmare like this one.
> For example, you find a simple line of C++ that says "y=f(x);", and it can do anything: has the parenthesis operator been overloaded? and the copy constructor? how many times is a destructor called here? What are its side effects? What instance of f is called, if none of them is defined for the actual type of x? Every single line of C++ that you have to analyze is a terrifying nightmare like this one.
I have yet to see any of this be an actual problem in real-world projects.
I have seen on the other hand a lot of people coming with these abstract "this is gonna be a problem" ideas in mind, and never wanting to touch C again after working on actual modern C++ projects.
Well, I for one had the opposite experience. I started programming in C++, and writing, reading and debugging lots of C++ code. It was not "modern" C++, since I stopped doing so around 2006. I embraced C out of personal despair with C++, especially the C++ of other people. For my personal projects, I can still enjoy using "my" favorite subset of C++ (basically, without objects nor pointers, and building the data structures by composing stl containers). Yet, anytime I have to deal with an unknown piece of C++ it is a nightmare. Especially since it is very easy to hide the behavior of the code.
Just on Monday, I was bitten by a bug caused by operator overloading. I'm building a static analysis tool in C++. There's a method `getType()` that returns the qualified type of an expression. I had a call to `getType()` nested in some other calls, e.g. `expr->asAssignmentExpr()->getRHS()->getType()->getSomeQualifiedTypeProperty()`.
The creators of the `QualifiedType` type that's returned by `getType()` had overridden the class member access operator to return an UnqualifiedType. This problem took me an embarrassingly long amount of time to diagnose. The compiler error messages told me that `UnqualifiedType` doesn't have a property `getSomeQualifiedTypeProperty`, but I couldn't see why I was getting an unqualified type from a method that is supposed to return a qualified type. I searched the documentation online, searched the header files, tried reinstalling the library, nothing made sense.
Then, I learned this operator had been overloaded. So, I just needed to write `expr->asAssignmentExpr()->getRHS()->getType().getSomeQualifiedTypeProperty()`.
Ouch.
with any decent IDE you'd type something like
expr.as<enter>.get<enter>.get<enter>.get<enter>
and you'd see the following:so I fail to see how this can be an issue if you don't use prehistoric tooling
Are there any languages since C you think have gotten it right?
Basically any language except C++ ? ;)
There's lots of languages that I use and enjoy: lua, julia, octave (just for numerical computation), python (except for numerical computation), the unix shell I find particularly beautiful and elegant (even with its obvious warts). Hell, even javascript the language is mostly OK, if it wasn't for its toxic development ecosystem.
In practice you know what it does because of the context. Devs don't usually override operator to mean the opposite of their meaning. And in case there are doubts, editors have become pretty good at pointing out at the right location.
It is the same in C. is `y` or `f` or `x` a macro that could potentially have `return` or `goto` statements, that can potentially use a `__LINE__` meaning you can't use two occurrence on the same line or within another macro. if `f` is a function, what are its side effects? Do conversion happen depending on the type of x? Or is it like a va_arg function that need the exact right type?
What i mean is that if you want to obfuscate things, you can do it in C just like in C++. But in normal code it's all pretty obvious.
C macros can be useful, I would enjoy programming in this language much less if they weren't there. And if I build a reasonable abstraction with them, it's as little as a grep or go-to declaration to find out the definition. There are no implicit complicated resolution mechanims, no complicated internal data structures that keep what should be application data isolated in the compiler (so it has to be duplicated in other parts of the code). With macros, I just search for the name, and will usually quickly find the definition with very little additional indirection.
In c++, the = could be a function too, that does unexpected things. Critically, if you are unaware of the side effects of that = operator, you may be doing things you don't want to do. The most likely issue would be performance if you are repeatedly using the = operator thinking it is a simple operation. Like if someone is being clever and uses the Active Record model and you are unexpectedly doing database writes with what you thought was a local object with no side effects.
To not have this happen, of course, you could read the entire object definition, but then you need everyone working on the project to have 100% of the code committed to memory. This is why you will likely find rules in the coding style guide of whatever company banning such practice after someone is bitten by it.
No way. The mental costs alone are staggering.
My standard observation was that to program C++ properly I always needed an entire shelf of books at hand and I used them daily.
The fact that C++ still can't deliver an FFI/name-mangling/v-table/etc. standard that everybody can agree on tells me that it's too complex.
I don't see the relation with the fact that msvc and GCC have chosen different c++ ABI. That's not telling anything about the complexity of the language.
Getting back to the "C way" takes a couple of months to get C++ out of your system first ;)
Of course for some types of problems, C++ is indeed the better language than C, but very often other languages are even better suited (e.g. I turn to Python for most problem where C doesn't work well, for instance munching text files)
Sometimes with defer() you might get a difficult to read error message when you're doing something wrong, but it's galactically easier to deal with than the standard STL error spew that so many C++ programmers have just come to accept, and it's hard to do something wrong in the first place (just put simple expressions as arguments).
A comparison of defer() with RAII is not that meaningful, as they are totally different things. RAII is this complicated system that leads to (requires) other more complicated stuff like {default,copy,move,assignment...}{constructor/destructors} as well as exceptions, with all sort of terrible interactions and implicit behaviour, that can easily get out of hand and make your code an unreadable mess and/or produce terribly inefficient (piece-meal allocations/function calls) and slow-compiling code (huge header includes).
In the simple situations where defer() is a win, if you use (just) RAII instead, you always have to declare a class out-of-line first (using weird braces and all) to get ad-hoc behaviour, while with defer() macro you can just make something up on the spot, which is often a better way.
I'm not saying there can't be experienced coders who use RAII well in some situations, but personally I opt to just not use it, since the price seems far to high for the benefits, outside of say primitive use in leetcode assignments.
for (int i = 0; i < files_count; ++i) {
file_handle_t handle = open_file(files[i]);
defer(close_file(handle)) {
// ...
if (some_condition) {
close_file(handle);
break;
}
// ...
}
}
But this doesn't work because you did not know or you forgot that `defer` was implemented with a `for` inside and so the `break` or `continue` goes out of the wrong loop. and that's just one example of the many thing that can go wrong because it is a macro.On the other hand, with RIAA, you just have your file_handle that automatically close so you can't forgot to close it, and the rules are well known and not that difficult to understand.
for DEFER(init, exit) {
}
or at least give it a name that indicates clearly that this is syntactically a loop. That's like with some iteration macros that I like to use as well from time to time. The basic idea is to never try to hide what you're doing, since that will backfire. You can still take a balanced approach with DRY and separation of concerns principles, which can lead to reasonable macros.> and that's just one example of the many thing that can go wrong because it is a macro.
I'm inclined to say that nothing can go wrong in above usage, or at the very least it would take criminal energy to break it (I really can't come up with a case right now).
It's perfectly doable to write safe macros with only a little experience. Follow these simple rules when defining macros: 1) parenthesize usages of macro params, and parenthesize the whole macro body if it is a complete expression. 2) Use parameters that take syntactical expressions only once, to prevent evaluating side-effects (like x++) multiple times.
Linux kernel even allows for declaration of local variables in macros by using expression statement blocks (GCC extension). Although I've never had a need for them, I believe that with this approach you can fulfill rule 2) in all possible situations.
You can improve macro safety in cases where you need to define a macro that is a little dangerous by just capitalizing the macro's name to make clear at the call site to be a little bit careful here. The thing is, there is no need to be 100% safe. It's very good to be 99% safe and have a simple to use mechanism. Because there are always so many other ways to break your programming, and if you're trying to be 100% safe at the cost of being twice as hard to use, that's effectively a net loss because you're provoking problems in other ways.
> On the other hand, with RIAA, you just have your file_handle that automatically close so you can't forgot to close it, and the rules are well known and not that difficult to understand.
Could agree if it's a file handle or something other off-the-shelf thing where you don't have to make the class yourself, and everybody instinctively is aware of the implicit behaviour. Otherwise, I very much like the more explicit behaviour and ad-hoc convenience that a defer() macro gives me.
Ah, classic no true scotsman'ism. No real C programmer would emulate classes, an object system, virtual dispatch, templates, ...
So you need a C++ mindset to have a use for a dynamic array, a hash table, threads or namespaces, all cross platform? (Yes windows is a platform too)
That's without considering raii! (which does admittedly have some baggage wrt the functions you need to implement.)
Namespaces: prefixes work just fine in C and don't require name mangling.
RAII makes no sense with C's concept of "dumb data" instead of C++'s "smart objects".
>Namespaces: prefixes work just fine in C and don't require name mangling.
This is a perfect example of a feature that solves a problem that C has (global namespace pollution). Prefixing is a bandaid, and a nasty one at that. Everyone loves #define NOMINMAX in the windows headers, and prefixing all their code with a faux namespace, dirtying _all_ of the code.
> RAII makes no sense with C's concept of "dumb data" instead of C++'s "smart objects".
Sure it does. Every call to fopen should be paired with a call to fclose, every malloc with a free. RAII practically eliminates a whole category of issues around resource leaks; in over a decade of writing C++ professionally, almost every instance of a memory leak, stomp or resource leak was someone trying to be "clever" and drop down to more C style code.
Anyway, even C has name mangling: For example, on Windows, all C symbols gets prefixed by an underscore https://docs.microsoft.com/en-us/cpp/error-messages/tool-err... And even on macOS there is something about having symbols starting with \1
So most UNIX/POSIX projects, even new ones, will default to C.
Then there is the embedded space where almost 40 years later trying to use C++ instead of C is still an uphill battle, between embedded development communities, tool vendors, debugging tools, available libraries.
Arduino and mbed are the exceptions, not the rule.
Even Microsoft despite all their security talk around Azure Sphere, just releases a pure C SDK for it.
Which is while I tend to rant about C, I also argue that tooling improvements related to security are more than welcomed, and knowledge about best practices when coding in C should be more widespread.
It is the COBOL of systems programming languages (about 10 years younger) and it isn't going away for the foreseable future.
And why do you think this is so?
When I was starting with embedded development I missed C++ as I felt compelled to reinvent same things that were given in C++.
But then I grew up to learn there is a hefty price to pay for it and in embedded development those problems are blown due to properties of the environment and types of projects you build.
It is one thing for a game to be constantly crashing and it is another if this is happening to an embedded device.
When you develop embedded code it is way more important to understand exact consequences of your actions and C++ is trying to hide those from you as much as possible.
There are also other requirements like being able to easily ascertain the amount of stack used, ability to use exclusively statically allocated memory, etc. that are conflicting with C++ or at least making it difficult to enforce them in C++.
I am very happy to see Rust is overtaking C++ in embedded and is an answer to C problems without creating (too many) of its own.
"CppCon 2016: Dan Saks “extern c: Talking to C Programmers about C++”"
https://www.youtube.com/watch?v=D7Sd8A6_fYU
"Writing better embedded Software - Dan Saks - Keynote Meeting Embedded 2018"
https://www.youtube.com/watch?v=3VtGCPIoBfs
"Embedded & C++ - Meeting 2017 Keynote"
https://www.youtube.com/watch?v=mNPfsUZb3vs
In a way, the same kind of subculture in C++ that jumps in cries of horror if you happen to use std::vector/array/string with bounds checking enabled.
When I worked on C++ projects half of the discussion was on just the use of language or various consequences of it.
On C projects that never happens. There is nothing to talk about so everybody focuses on making the project better.
Things I don't miss after I switched from C++ to C:
-- discussing compiler feature set,
-- discussing how exactly to format complex C++ constructs,
-- regular calls for help from less experienced devs to more experienced C++ "gurus" for their wisdom to just interpret what the code does or tell them how to write the thing they want to write,
-- trainings for less experienced developers so that they can even understand the syntax of the code,
-- hunting bugs due to lack of understanding of complex internal mechanisms,
-- having hard time working with a debugger on complex code,
-- not being able to tell wtf is the exact type of the thing that I am looking at or what exact code is going to get execute when I use an operator or call a function. The IDE not being able to locate it for me is an added "bonus".
Then there are the discussions on:
- what warnings to turn on as errors
- what ISO C says and what compiler XYZ does, with many not even aware to the differences
- not clear understanding of the 200+ UB cases documented on the standard
- in-house libraries for safer handling of strings and arrays
- experienced developers creating CVEs because they think they are free of such errors (pun intended)
Now if people choose to ignore the tools available to them, just like they enjoy ignoring lint since 1979, well there is little the doctor can do to save patients that refuse help.
This is just libel, really. There are meta-nerds in C and C++, but there is also a great majority of programmers just interested in delivering good software.
I worked in embedded development for over 10 years and at every place it was the norm to use C++ instead of C internally. But when creating libraries for customers to use, these where almost always in C or at least with C bindings.
I guess the reason is that one can use C libraries in C++, but not the other way around.
I can write VB/Delphi like code with C++ Builder, write raytracing algorithms in CUDA, or just have fun targeting ESP32.
IMO that really depends on the project size and goal of the project. I've written a C++ project of about 1000 lines (algobot trader with some optimized data structures in keeping the order book consistent). I've written it in VS Code, used a GUI debugger and I haven't really paid for anything.
Meanwhile, I have this friend that programs so much C++ that his mantra is "in C++ assume all tools are broken." When I hear his horror stories, oh dear! Heisenbugs come by every 2 to 3 months.
So for small projects that aren't touching the lower OS/systems level parts, you're not paying any extra costs. However, the deeper and further you go, I'd fully agree.
My largest C project was 60k lines of code.
The code was very, very dense. It had a lot of functionality that had to be packed to as little code as possible (due to memory limitation). It had things like transactional log-based database working in very small, static amount of memory, point in time recovery (so if the application failed it could restore itself to a number of possible checkpoints before the failure), parsers for various protocols (BER-TLV, for example), a lot of cryptography and custom cryptographic protocols, a GUI framework for a small monochromatic screen and keypad, drivers for a bunch of devices (thermal printer, external pinpad, etc.), SSL, a lot of business logic, state machines for talking to a network host, etc.