The one real reason to use C++ is for templates. Exceptions, object orientation, etc. is all fluff. C++ is far better captured as “C with metaprogramming” rather than “C with classes”, and the other features are the supporting cast for that, not the other way around.
RAII strictly for preventing leaks of resources is the lone thing I miss in C.
Constant folding via constexpr in C++ is much much better than C.
Templates help write generic code that results in specialized machine code; it may be hyper optimized at the cost of binary size. They make it harder to write code that at runtime is a little more type generic (if that makes sense). You can't always afford those additional copies generated. Not that it's a bad thing; just an observation as I write both strictly C and C++ at my day job.
Stealing the below; string handling is significantly safer with std::string. I'm not sold on std::string_view.
You can control code generation for templates by putting only the generic data structures in the header, and all function bodies in an implementation file. Then you explicitly instantiate the template for the types you want it for in that implementation file, and declare those instantiations as forward references in the header.
It's not pretty, but it's not hard either, and gives you full control over how many instances there are around. When C++ is used in a heavily constrained environment, that can come in handy.
The bug there is implicitly converting an r-value to a non owning reference. We wouldn't call the same semantics a bug with a 'char const*' to be fair. We would complain that string implicitly converts or reject the application code as buggy.
That being said, yes, string semantics are hard. Passing around copies of a string buffer (std::string) or calling to_string_view everywhere don't seem better.
C++ also has a fair number of built in STL smart-pointer types that do approximately the same thing as the "move/borrow" semantics which everyone gets excited about in Rust.
These alone are a worthwhile "dialect" on top of C, the use of which results in a far safer C; and—as far as I know—these smart-pointer types and their compile-time semantics would be impossible to implement without C++'s additional layered-on "C with classes" type system.
(It's clear that these smart-pointer types are a valuable addition to C, because they were independently implemented as part of all sorts of other C macro libraries in the early 90s. I think there were at least four C smart-pointer implementations that existed as part of various parts of Windows, for example.)
STL smart pointers are all fun and games until you start using threads, at which point you realize that automagically flipping the (now atomic) counter back and forth behind the scenes every time the pointer can't be safely passed as a reference is a performance disaster.
Manually referencing and de-referencing exactly when needed avoids most of this. Sometimes it's perfectly safe to pass the same reference through several layers without changing the number of references.
As an added bonus YOU get do decide which references, if any, need to support multiple threads.
Reference counted smart pointers are just one of the many smart pointer types. Atomically reference counted smart pointers are a smaller subset of those.
I think the GP was accusing you of "side tracking" by bringing up std::shared_ptr in the first place.
My post above was about how C++ smart-pointers compare favorably to Rust's oft-considered-"unique" ownership/borrowing system. The particular STL features that cause equivalent "compile-time magic" to happen to what you get from Rust's semantics, are those of std::uniq_ptr and std::move, not those of std::shared_ptr. So the value (or lack thereof) of std::shared_ptr, isn't really a knock against the value of the C++ STL smart-pointers collection as a whole—you can entirely ignore the existence of std::shared_ptr (I do!) and still think C++ STL smart-pointers are the cat's pyjamas.
True it requires classes as RAII is based on a destructor. However this doesn't require OOP design, which typically is associated with classes and objects.
You don't have to use inheritance, but you will certainly use object composition and encapsulation to manage the lifetimes of your data in a coherent way.
Umm..things like standard library strings, futures, threads, memory management via shared_ptr, unique_ptr, RAII etc are also other great reasons to use C++. Every time I attempt to code in C, its like my hands and legs are cut-off. I suppose some folks enjoy this.
The biggest problem with C isn't even it's lack of type safety, undefined behavior but that antiquated macro system which does more harm than anything.
Imagine if C had a solid AST-based macros system...
If you're interested in alternate languages along the lines of 'C with metaprogramming' (and without the bad stuff C++ forces you to deal with in some cases), you may enjoy taking a look at Zig (https://ziglang.org/) or Rust (+ RAII).
This is actually one of the strongest arguments against writing performance sensitive code in C++.
Because of operator overloading, I don't know if `A * B` is incurring a (nontrivial, arbitrary) function call. Maybe it's just normal scalar multiplication, maybe it's finding the dot product of two arbitrarily sized matrices.
Having used multiple maths libs over the year I have never been in a position when I did not know immediately what A * B does. Why are you writing it if you don't know what happens ? At which point in your life would you write A * B and not know if one of A or B is a matrix ?
You'll know what it does if you're the author. But code is read much more often than it's written, and it's easier to spot bugs and understand what each line of code does without operator overloading. Operator overloading obscures what the actual code is doing. Because of this, it's is generally not used in kernels, virtual machines, etc.
As an example, if I give you this line of C++ code, and no other information (which would require further effort to determine, e.g. the types of identifiers), can you tell me if it's a function call, performs I/O, or does dynamic memory allocation?
Now tell me if it involves a function call, performs I/O, or does dynamic memory allocation?
And, of course, things like macros and typeof are totally used in kernels, virtual machines, etc... The ultimate obfuscation is alive & well in those areas. So no, the potential to do terrible things in operator overloading is not even remotely a concern there. No more than anything else.
That's not equivalent C code - because there is no equivalent C code.
I can tell you, if I write this in C, it performs no I/O, memory allocation etc:
typeof(A) C = A + B;
I know for a fact that (modulo people doing incredibly stupid things with macros), that this is simple arithmetic addition. I don't know that for a fact in C++ - because of operator overloading.
> I know for a fact that (modulo people doing incredibly stupid things with macros), that this is simple arithmetic addition. I don't know that for a fact in C++ - because of operator overloading.
The only way to know for a fact what happens in either C or C++ is to know the types of A & B, which you (seemingly intentionally) omitted.
But if A & B are the types that can be added in C, then it's going to be literally identical in C++ with no chance of operator overloading. If A & B are not types that can be added in C, then you would see an add() function called instead and you'd be right back to not knowing what it does.
Yes, I am intentionally not looking at the types, because that's something that might not be incredibly apparent in C++ (or C).
We're violently agreeing at this point. Yes, in C, this problem does not exist, because operator overloading does not exist.
In C++, what `A+B` does is ambiguous without knowing 1) what are the types of A and B, and 2) what are all the possible `operator+` overloads that exist which may operate on A and B. It's a hidden complexity which doesn't make potentially expensive operations apparent, and kernels and VMs hate that kind of thing.
> because that's something that might not be incredibly apparent in C++ (or C).
Yes, it is. Extremely so.
> Yes, in C, this problem does not exist, because operator overloading does not exist.
Except it totally still does because you never know what function add does for random types. It's literally the same exact thing. Function calls might be expensive and they might not be.
> It's a hidden complexity which doesn't make potentially expensive operations apparent, and kernels and VMs hate that kind of thing.
No, it really isn't. The add overload operator is never non-obvious, and never actually does memory allocation, file io, or any of that other nonsense.
Can you be intentionally stupid about it? Yes, sure. Is that an actual concern that anyone working on a VM or kernel should have? No, not in the slightest. Completely nonsense.