Webinar: C++ semantics - 06.11
In this article, I'll try to list all things that we can throw out of C++ without remorse. This won't cost us anything but will reduce the standard (and our headaches), take the burden off the compiler developers and students who learn this language — and thus will eliminate the memetic potential of the C++ enormity.
We published and translated this article with the copyright holder's permission. The author is Kelbon (kelbonage@gmail.com). The article was originally published on Habr.
This article may express some controversial points but we believe it is also a thought-provoking piece. Please note that this is a guest article. The opinions expressed within the content are solely the author's and may not reflect the opinions and beliefs of the PVS-Studio team.
Subscribe to our newsletter so as not to miss the most interesting articles on our blog.
First of all, I want to remove the thing that often causes errors and pulls back the language development. And by that I mean union, a sum type from the 70s. In the C language, the idea of storing several types within a single memory fragment is still relevant — all types in C are a set of bytes with a fixed size.
However, using union in C++ is an automatic undefined behavior. Look:
#include <string>
union A { int x; float y;};
union B {
B() {} // you need to write a constructor and a destructor here
// but it's impossible to write a destructor correctly
// if you don't believe me, try to do it yourself
~B() {}
std::string s;
int x;
};
int main() {
A value;
value.x = 5;
value.y; // undefined behavior, accessing the inactive union member
B value2;
value2.s = "hello world";
// undefined behavior, the s field is inactive and in use
// (operator = for std::string)
}
As you can see, it's impossible to use union without errors. Moreover, you have to manually call the correct destructor for an object all the time, and use the placement new instead of the assigning operator in the desired field. So why suffer when you can make a good type with a good interface WITHOUT any overhead relative to union?
The following code completely replaces union, doesn't have any overhead relative to it, and has a more user-friendly interface (emplace / destroy).
template<typename ... Ts>
struct union_t {
alignas(std::ranges::max({alignof(Ts)...})
std::byte data[std::ranges::max({sizeof(Ts)...});
template<one_of<Ts...> U>
constexpr U& emplace(auto&&... args) {
return std::launder(new(data) U{std::forward<decltype(args)>(args)....});
}
template<one_of<Ts...> U>
constexpr void destroy_as() const {
reintepret_cast<const U*>(reinterpret_cast<void*>(data))->~U();
}
};
Besides, the standard has a colossal number of exceptions for union, useless rules and restrictions. And this thing can be abandoned and forgotten. It's 2022, for God's sake...
This may sound weird, but we can remove arrays from C++ and lose nothing. We can get rid of this awful syntax — char(&&...arr)[N] (take a guess in comments what that means).
In addition, arrays can't be copied and can't do move semantics, which makes them the most inferior types in C++.
How can they be replaced? With a recursive/"multiple inheritance based" tuple with elements of the same type (this was obvious).
Fun fact: The C++ standard has an exception even for the for loop with C arrays... Which confirms the obvious — arrays poorly correlate with the rest of the language.
template<typename T, size_t I>
struct array_value { T value; };
template<typename, typename>
struct array_impl;
template<typename T, size_t... Is>
struct array_impl<T, std::index_sequence<Is...>> : array_value<T, Is>...{};
template<typename T, size_t N>
struct array_ : array_impl<T, std::make_index_sequence<N>> {
// some array interface at your wish
T& operator[](size_t n) {
// such an implementation here is for short
return *(reinterpret_cast<T*>(reinterpret_cast<void*>(this)) + n);
}
};
void mostly serves for making exceptions for it in generalized code. It would be more convenient to have a type with a single meaningless value. Oh, if only there were a way to do it...
struct [[maybe_unused]] nulltype {};
// That's it. Even the [[maybe_unused]] attribute is here purely for beauty
Seems like the stakes are getting higher, what does the author want to remove this time? int?!
Yep, C had bad types and C++ inherited them. Who on earth would use int that may take up to 8 bytes but guarantees its values only up to 2 ^ 16??? This is literally an error maker (especially for junior developers).
You can replace it with the byte fundamental type and pointers. Indeed: with byte and the C++ type system you can create any types even similar to int, double, float, bool, etc., from fundamental set.
Here we kill several birds with one stone — no more exceptions for fundamental types in overload resolution and no more exceptions in boilerplate code for inheritance (you can't inherit from fundamental types). Besides all small exceptions for such types become a thing of the past.
This. Shouldn't. Compile. At. All. (But it does): https://godbolt.org/z/fz6eMEeqG
int main() {
(void)(5), (void)5, void(5);
}
The person who came up with this thing in C raised holy hell and now we have to live in it.
Even despite the fact that notorious printf(const char* pattern, ... ) is implemented using runtime variadic arguments (If you don't get it, ellipsis replaces runtime arguments! Any arguments can be there!), this still looks like the biggest kludge in the history of programming. And how to use it... Eh... The __VA_START__, __VA_COPY__ macros and a bunch of stuff along with it will haunt C developers in their nightmares for ages. The C++ developers should just exorcise this demon out of the language and add new features for template parameter packs instead.
Everything is simple here, C++ has a perfect replacement for this word. Just compare:
// foo is now alias for void(*)(int) (a pointer to a function)
typedef void(*foo)(int);
// same thing but in C++
using foo = void(*)(int);
There's no point in leaving typedef in C++...))
These are macros from C that take arguments. They are usually the main reason for difficulty of understanding code — bad code, because in modern C++ using such macros is unacceptable.
So, at this point we removed almost all C from C++ and got almost clean ++. Let's look at what we can remove from them too.
Indeed, why do we need these operators if everything has long been moved to the abstraction level of allocators, and we can continue allocating memory on low levels via malloc?! How could anyone bring the system memory allocator to the core language level?
Just look at the list of new overloads (and I don't even mention the rules):
We only need to leave the placement new to call constructor at the desired address. Everything else, especially overloads of new/delete, is strictly prohibited to use in modern C++, unless you want to be bullied.
Here's a link [RU] to my own article about the uselessness of this keyword. The brief summary is under the spoiler.
When classes were added to C++, it was the time of OOP, encapsulation, inheritance, all that. Therefore, it was decided that class is privately inherited by default, and the class fields are also private by default. Time shows that private inheritance is a rare thing, almost non-existent in real code. Besides, you always have something public but not always something private.
Initially, struct in C didn't have the capabilities of class in terms of adding member functions, constructors, and destructors. Now struct differs from class solely by these two default parameters, which means each time you use class in your code, you just add a redundant line.
But class has many more meanings!
In a template: to me, the only use of this feature in 2022 is to confuse the reader. Although some still use it for the sake of saving 3 letters. We are not to judge them.
In a template but to declare template parameters — in C++17 this feature has become obsolete. Now you can write typename without any problems.
So, to sum up: at the moment there's no real need to use the class keyword in C++, which is funny.
Wait, there's also enum class! You won't believe it, but struct works here too:
enum struct Heh { a, b, c, d };
In conclusion, starting with C++, there's not a single contextual use of class where it can't be abandoned or replaced with other keywords.
Has no helpful use case scenario known to me, breaks the generalized code, the verdict — remove.
They cause a huge number of errors.
They are inefficient, make you write architecturally bad solutions, use memory poorly, don't allow you to use the rest of the language if the virtual keyword is used. THE MOST IMPORTANT PART — they can be easily replaced with other core language features without losing functionality but with gained performance, usability, code repetition, checks for compilation, etc.
Implementation of dynamic polymorphism without virtual functions and their problems: https://github.com/kelbon/AnyAny .
C++23 (finally) introduced deducing this, thanks to which you can declare passing this to the type member functions. This "member function" will actually be a function from the language's point of view. Thus, further (along with deleting virtual functions) you can get rid of the VERY CONCEPT of member functions and a pointer to this thing (God forbid you to ever see declaration of the pointer to the member function).
struct A {
void foo(this A& self);
};
At the same time, it is possible that gradually the this keyword will lose its former meaning and only one will remain — the declaration of the explicit passing the type reference/value to the function.
That's all folks! We had a wonderful dream about holy C++ and now it's time to write crap with virtual inheritance, forgotten virtual destructor on a polymorphic type, and casts from C. May the odds be with you...
0