> No reflection ... impossible to ... iterate over the methods or the attributes or the base classes of a class ... [or] programmatically determine the type of an object...
There's run-time reflection, and compile-time reflection. C++ has almost no runtime reflection - and this is again due to performance considerations. That would have a lot of overhead.
As for compile-time reflection, C++ has gained some over the past decade. decltype() gives you the type of an object. A static reflection Technical Specification is out, see: https://en.cppreference.com/w/cpp/experimental/reflect . It will allow much of what the author was decrying. It may make it into C++23, and some compilers already partially-support it. Now Is it very late in arriving? I suppose it's fair to say that. But TBH, it's quite difficult to design this for a 40-year-old language with a lot of baggage and not within a managed runtime. And achieve near-consensus about it.
----
> Very complicated type system
The type _system_ of C++ is not very complicated. What the author seems to dislike is how common standard-library types are actually somewhat complex constructs within the type system rather than atomic types or simple constructs. A language in which "string" is an atomic type might have std::map<string, string> rather than the complex-sounding `map<std::basic_string<char, std::char_traits<char>, std::allocator<char>>`, `std::basic_string<char, std::char_traits<char>, std::allocator<char>> >`. Still, that longer type is simply `std::map<std::string, std::string>`, and if you `use std::string;` then it's `std::map<string, string>`. (Yes, there are other template parameters to map, I know, I'm making a point).
In fact, one could argue that it is a virtue of C++ of having a non-complex type system: The complex types are actually implemented _in C++_, with no need for a rich gallery of atomic types. Other systems put things like lists, associative arrays, optionals (a.k.a. maybe's) etc. out of the reach of "mere mortals".
Then again, the choice of primitive types or type constructs in C++ is not great due to inheriting from C. C++ can construct new types as arrays-of, structs-of, unions-of, references-to, pointers-to and bit-fields-of. arrays are annoying with their decay behavior and unassignability, and non-discriminated unions are also useful mostly for low-level works. It would perhaps have been nicer if arrays behaved like std::arrays and unions like std::variants, and for the C-style types we would have used reinterpret-casts and other dirty tricks. But then, what would you want to happen when a constituent element of a variant throws an exception during construction? Is the variant in a valid state after that exception is caught? The answer is not at all trivial, so richer atomic types require making choices that aren't always right for everyone.
> if your function accepts const std::vector<const char*>& ... and I have a std::vector<char*> object ...then I can't pass it to your function
Well, not exactly. I mean, you actually can pass it to the function, if you insist: `my_func(reinterpret_cast<std::vector<const char*>&>(my_vec)` ... but that's not very nice. So, yeah, this is definitely an issue.
However:
1. There is no inherent reason that type T<E*> should be usable as a T<const E*>. Those could be completely different types For T = std::vector the types just happen to correspond nicely. I doubt that other languages address this, though.
2. If you want to be a wise guy, and tell me "other language would just have a string and a const string instead",, then - you wouldn't have this problem, since there is no such thing as an std::vector<const E>; that doesn't compile.
3. If your function takes `std::span<const some_string_class>`, you again don't have this problem.
so, a bunch of mitigating arguments + you can still do it if you really want to.
----
> Very complicated type-based binding rules
They're only complicated if you make them complicated. If you single f(), there won't be any complex overload resolution; and if you write a bunch of f()'s, there will.
As for the standard library, it's overload resolution can get complicated; but error messages have improved significantly! First, we got static_assert()'s in C++11 which explain what's the problem with (hopefully) simple text. Then in C++17 we got some deduction guides, so you fail less often. Then in C++20 we got concepts - which themselves are quite complex, but you don't have to worry about them; the point is that your error messages for overload resolution improve again. Don't get me wrong: There will still be long errors about failure to resolve, but they're more readable these days.
----
> Defective operator overloading
The only argument I saw here was:
> overloaded operators have to return their results by value - naively returning references to objects allocated with new would cause temporary objects to "leak" ...
1. In C++, we pass things by value unless there's a good reason not to.
2. If you're worried about copying when returning - that doesn't happen. This used to be a compiler optimization, but since C++17 the language guarantees it: https://en.cppreference.com/w/cpp/language/copy_elision . The return type doesn't even have to be copyable or movable!
3. Don't use new, it's rude these days: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines... ; if for some reason you must allocate dynamic memory, use std::unique_ptr, or std::shared_ptr, or return an allocating container class instance, by value.
---
> Defective exceptions
C++ exceptions could of course be improved in various ways (e.g. they may soon be getting stack traces); but not along the author's line of argumentation.
> If we use exceptions, we have to write exception-safe code - code which frees all resources when the control is transferred from the point of failure
You need to write "exception-safe" code anyway! Acquire resources on construction, release them on destruction. Also known as CADR or RAII.
> ... and the vast majority of "resources" happens to be memory, which is managed manually in C++
Effectively - no, it isn't. And that's exactly because we're writing that "exception-safe" code: Since we _don't_ just write new nor delete (see above)
> To solve this, you are supposed to use RAII ... meaning that all pointers have to be "smart"
1. No they don't - only the _owning_ pointers need to be smart (or in container classes etc.); and the non-owning pointers you don't have to free.
2. What's your obsession with pointers, anyway? Pointers in C++ are so last-millenium...
----
> Duplicate facilities
Yeah, C++ has some of those. I mentioned std::array vs C arrays and std::variant vs C unions. That's the backwards compatibility for you... but also, don't mistake _similar_ facilities for _duplicate_ ones, like the author does:
> If you need an array in C++, you can use a C-like T arr[] or a C++ std::vector<T>
Those are different! `T arr[N]` has fixed size determined at compile time, and its elements typically resides on the stack; `std::vector<T>` has variable size and its elements typically reside on the heap (allocated with new). In fact, C++'s standard library is _missing_ many important array-like classes! See: https://stackoverflow.com/a/67409339/1593077
> That's because C++ doesn't have garbage collection, since that, folks, is inefficient.
----
> No reflection ... impossible to ... iterate over the methods or the attributes or the base classes of a class ... [or] programmatically determine the type of an object...
There's run-time reflection, and compile-time reflection. C++ has almost no runtime reflection - and this is again due to performance considerations. That would have a lot of overhead.
As for compile-time reflection, C++ has gained some over the past decade. decltype() gives you the type of an object. A static reflection Technical Specification is out, see: https://en.cppreference.com/w/cpp/experimental/reflect . It will allow much of what the author was decrying. It may make it into C++23, and some compilers already partially-support it. Now Is it very late in arriving? I suppose it's fair to say that. But TBH, it's quite difficult to design this for a 40-year-old language with a lot of baggage and not within a managed runtime. And achieve near-consensus about it.
----
> Very complicated type system
The type _system_ of C++ is not very complicated. What the author seems to dislike is how common standard-library types are actually somewhat complex constructs within the type system rather than atomic types or simple constructs. A language in which "string" is an atomic type might have std::map<string, string> rather than the complex-sounding `map<std::basic_string<char, std::char_traits<char>, std::allocator<char>>`, `std::basic_string<char, std::char_traits<char>, std::allocator<char>> >`. Still, that longer type is simply `std::map<std::string, std::string>`, and if you `use std::string;` then it's `std::map<string, string>`. (Yes, there are other template parameters to map, I know, I'm making a point).
In fact, one could argue that it is a virtue of C++ of having a non-complex type system: The complex types are actually implemented _in C++_, with no need for a rich gallery of atomic types. Other systems put things like lists, associative arrays, optionals (a.k.a. maybe's) etc. out of the reach of "mere mortals".
Then again, the choice of primitive types or type constructs in C++ is not great due to inheriting from C. C++ can construct new types as arrays-of, structs-of, unions-of, references-to, pointers-to and bit-fields-of. arrays are annoying with their decay behavior and unassignability, and non-discriminated unions are also useful mostly for low-level works. It would perhaps have been nicer if arrays behaved like std::arrays and unions like std::variants, and for the C-style types we would have used reinterpret-casts and other dirty tricks. But then, what would you want to happen when a constituent element of a variant throws an exception during construction? Is the variant in a valid state after that exception is caught? The answer is not at all trivial, so richer atomic types require making choices that aren't always right for everyone.
> if your function accepts const std::vector<const char*>& ... and I have a std::vector<char*> object ...then I can't pass it to your function
Well, not exactly. I mean, you actually can pass it to the function, if you insist: `my_func(reinterpret_cast<std::vector<const char*>&>(my_vec)` ... but that's not very nice. So, yeah, this is definitely an issue.
However:
1. There is no inherent reason that type T<E*> should be usable as a T<const E*>. Those could be completely different types For T = std::vector the types just happen to correspond nicely. I doubt that other languages address this, though.
2. If you want to be a wise guy, and tell me "other language would just have a string and a const string instead",, then - you wouldn't have this problem, since there is no such thing as an std::vector<const E>; that doesn't compile.
3. If your function takes `std::span<const some_string_class>`, you again don't have this problem.
so, a bunch of mitigating arguments + you can still do it if you really want to.
----
> Very complicated type-based binding rules
They're only complicated if you make them complicated. If you single f(), there won't be any complex overload resolution; and if you write a bunch of f()'s, there will.
As for the standard library, it's overload resolution can get complicated; but error messages have improved significantly! First, we got static_assert()'s in C++11 which explain what's the problem with (hopefully) simple text. Then in C++17 we got some deduction guides, so you fail less often. Then in C++20 we got concepts - which themselves are quite complex, but you don't have to worry about them; the point is that your error messages for overload resolution improve again. Don't get me wrong: There will still be long errors about failure to resolve, but they're more readable these days.
----
> Defective operator overloading
The only argument I saw here was:
> overloaded operators have to return their results by value - naively returning references to objects allocated with new would cause temporary objects to "leak" ...
1. In C++, we pass things by value unless there's a good reason not to.
2. If you're worried about copying when returning - that doesn't happen. This used to be a compiler optimization, but since C++17 the language guarantees it: https://en.cppreference.com/w/cpp/language/copy_elision . The return type doesn't even have to be copyable or movable!
3. Don't use new, it's rude these days: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines... ; if for some reason you must allocate dynamic memory, use std::unique_ptr, or std::shared_ptr, or return an allocating container class instance, by value.
---
> Defective exceptions
C++ exceptions could of course be improved in various ways (e.g. they may soon be getting stack traces); but not along the author's line of argumentation.
> If we use exceptions, we have to write exception-safe code - code which frees all resources when the control is transferred from the point of failure
You need to write "exception-safe" code anyway! Acquire resources on construction, release them on destruction. Also known as CADR or RAII.
> ... and the vast majority of "resources" happens to be memory, which is managed manually in C++
Effectively - no, it isn't. And that's exactly because we're writing that "exception-safe" code: Since we _don't_ just write new nor delete (see above)
> To solve this, you are supposed to use RAII ... meaning that all pointers have to be "smart"
1. No they don't - only the _owning_ pointers need to be smart (or in container classes etc.); and the non-owning pointers you don't have to free.
2. What's your obsession with pointers, anyway? Pointers in C++ are so last-millenium...
----
> Duplicate facilities
Yeah, C++ has some of those. I mentioned std::array vs C arrays and std::variant vs C unions. That's the backwards compatibility for you... but also, don't mistake _similar_ facilities for _duplicate_ ones, like the author does:
> If you need an array in C++, you can use a C-like T arr[] or a C++ std::vector<T>
Those are different! `T arr[N]` has fixed size determined at compile time, and its elements typically resides on the stack; `std::vector<T>` has variable size and its elements typically reside on the heap (allocated with new). In fact, C++'s standard library is _missing_ many important array-like classes! See: https://stackoverflow.com/a/67409339/1593077
> That's because C++ doesn't have garbage collection, since that, folks, is inefficient.
It's not just inefficient; by now, it's effectively unnecessary. See: https://stackoverflow.com/a/48046118/1593077