Why C++ is the worst language
Some notes
C++ is absolutely atrocious. First of all there are like 20 different ways to initialise a variable.
int x; // default initialisation: uninitialised (garbage value)
int x{}; // Value initialisation: 0
int x = {}; // Value initialisation (with equals 0)
int x = int(); // value initialisation (creates temporary, then copy): 0
int x = 10; // copy initialisation: 10
int x = int(10); // copy initialisation (creates temporary, then copy): 10
int x = (1, 0); // copy initialisation (comma operator evaluates to rightmost): 0
int x(10); // direct initialisation: 10
int x{10}; // direct list (uniform)initialisation: 10
int x = {10}; // copy list initialisation: 10
auto x = 10; // type deduction: int, value 10
auto x{10}; // type deduction: int, value 10 (since C++17, earlier was std::initializer_list<int>)
auto x = {10}; // type deduction: std::initializer_list<int> with one element 10
auto x = int{10}; // type deduction: int, value 10
auto x = (1, 0); // type deduction: int, value 0 (comma operator)
Point p = {1, 2}; // aggregate initialisation
Point p = {.x = 1, .y = 2}; // designated initialisation (C++20/23)
auto [a, b] = std::pair{1, 2}; // structured binding initialisation
std::vector<int> v = {1, 2, 3}; // std::initializer_list initialization
auto* p = new MyClass{1, 2}; // dynamic allocation with brace init
constexpr int x = 42; // constant expression initialisation
auto f = [ x = 42 ]() { return x; }; // lambda init-capture
foo({1, 2}); // braced initialiser as function arg
And think about this:
std::print("finally!");
This is only available from C++23 onwards - and the majority of the industry won't be using "cutting edge" stuff like this for a while.
Even writing a benchmark is insanity.
In javascript we can just do:
console.time("lol");
functionToBenchmark();
console.timeEnd("lol");
And it will give you the time taken in milliseconds.
In C++ we have to do:
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
FunctionToBenchmark();
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point::duration diff = t2 - t1;
std::chrono::microseconds duration = std::chrono::duration_cast<std::chrono::microseconds>(diff);
std::cout << "Time taken: " << duration.count() << " microseconds" << std::endl;
And this is just one version of it.
Casting in C++
Casting in C++ is another thing which is obnoxiously verbose in comparison to other languages.
In Java we do:
int i = (int) myFloat;
In C++ you have to do:
int i = static_cast<int>(myFloat);
It's incredibly annoying and tedious to type this out everytime you want to cast something. And it's a terrible eyesore that bloats up the code.
Consider any function or formula where you need to perform a cast multiple times. It makes it hard to read.
// converting between different numeric types in a physics formula
double calculateForce(int8_t mass, uint16_t acceleration, float distance, long coefficient) {
return static_cast<double>(mass) * static_cast<double>(acceleration) /
(static_cast<double>(distance) * static_cast<double>(distance)) * static_cast<double>(coefficient) * (1.0 + static_cast<double>(static_cast<int32_t>(acceleration) & 0xFF) / 1000.0);
}
And static_cast doesn't work in all scenarios either. Sometimes you have to use dynamic_cast, reinterpret_cast, const_cast, or bit_cast, depending on the context.
// static cast: safest general-purpose cast for common conversions.
double d = 123.45;
int i = static_cast<int>(d);
// dynamic cast: used for polymorphic types (classes with virtual functions). safe downcasting with runtime type checking.
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
// reinterpret cast: low-level, raw bit-wise conversion. raw memory reinterpretation (dangerous)
uintptr_t address = reinterpret_cast<uintptr_t>(ptr);
// const cast: used to remove constness from a pointer or reference, in another word adds or remove consts qualifier
const char* str = "hello";
char* writeable = const_cast<char*>(str);
// bit cast (C++20): safe bit-pattern reinterpretation.
float f = 3.14f;
uint32_t bits = std::bit_cast<uint32_t>(f);
The common theme i see with beginners is that they get annoyed with reading and writing static_cast. So they created an alia for it (like this).
// "technique" to make static_cast more succinct - don't do this
template<typename To, typename From>
To sc(From&& source) {
return static_cast<To>(std::forward<From>(source));
}
// before (recommended):
int i = static_cast<int>(3.14);
// after (less verbose but not recommended):
int i = sc<int>(3.14);
But creating aliases for language keywords is a really bad practice. Because other developers won't be familiar with your version of the language. So it's best not to do this.
And that's the thing that's so jarring about C++. Correct C++ code often just looks wrong, especially for beginners.
// non-idiomatic "Java style" C++
MyClass* obj = new MyClass();
// idiomatic C++
std::unique_ptr<MyClass> obj = std::make_unique<MyClass>();
And it takes a long time to build up an intuition for what good C++ code is supposed to look like. Eventually you ended up accepting the fact that in C++, the terse solution is almost never the correct solution.
Keywords
Another example that's fundamental basic thing in any other languages but complicated in C++ is creating a global variable - watch this 20 mins video on how to do this in C++ which would take 2 seconds to learn in any other languages.
The problem is that you have all these keywords like extern, const, inline, static and constexpr and they all mean different things in different contexts. And you have different idiomatic combinations of them. They also have their own rules and meanings in different situations. C++ aggressively repurpose existing keywords in inconsistent ways.
// Makes a constant accessible from other files (exxternal linkage).
// Useful for shared constants needed across multiple .cpp files.
extern const int BUFFER_SIZE = 1024;
// File-private compile-time constant that doesn't generate address references.
// Good for optmisation when the constant is only needed in one file.
static constexpr double TAX_RATE = 0.05;
// Function that can be evaluated at compile-time and has a single definition across files. Reduces code duplication while enabling compile-time computation.
inline constexpr double kelvinToCelsius(double kelvin) {
return kelvin - 273.15;
}
// Modern way to define compile-time class constants without separate definition. Combines zero runtime overhead with single-instance storage.
class Config {
inline static constexpr int MAX_CONNECTIONS = 100;
}
static
The static keyword for example, has about three or four use cases depending on how you count it.
// 1. A static variable in a function persists between function calls.
void func() {
static int counter = 0;
counter++;
}
// 2. A static class members exists independently of any instance / share class variables across different instances.
class MyClass {
static int counter;
static void method();
}
// 3. The third use case is a complete semantic non sequitur. Static variables and functions can only be accessed from the cpp file which they are defined in. It's used to make functions private.
static int value;
static void helper();
I really wished they would have used the word private or internal instead of static. This illustrates how C++ keywords often times don't even describe what they do.
inline
The inline keyword is also a misnamer.
In the past, it was used to inline functions - which is an optimisation trick to get the compiler to copy the code from the function definition directly into each call site rather than performing a normal function call. This helps avoid the overhead of function calls like settings up the stack, passing the parameters and return value handling.
But nowadays compilers are smart enough to do this automatically. So using the inline keyword for this purpose doesn't have any guarantees. These days, inline is used to resolve issues regarding the One Definition Rule.
The funny thing here is that using inline on a function does the exact opposite of using inline on a variable. In both cases, they resolve One Definition Rule related issues, but an inline function leads to potentially many separate instances of the function across the binary.
| Context | Explanation |
|---|---|
inline function | The inline keyword suggests to the compiler that the function body should be inserted directly at each call site rather than making a function call. This can result in potentially many copies of the function code throughout the binary, one at each location where it's called. The compiler may ignore the suggestion if it determines inlining would be detrimental (e.g., large functions). Modern compilers often inline functions regardless of the keyword when optimisation is enabled. |
inline variable | An inline variable (C++17) tells the compiler and linker that this variable should have exactly one instance across all translation units, even if the definition appears in header included in multiple source files. Before inline variables, you had to use workarounds like the Meyers singleton pattern or extern declaration to achieve this. Useful for constants or global state defined in headers (like V8 Isolate). |
Additionally you can also make a namespace inline - this is typically used for library versioning.
namespace lib {
inline namespace v1 {
void func() { /* v1 implementation */}
}
namespace v2 {
void func() { /* v2 implementation */}
}
}
int main() {
lib::v1::func(); // calls v1
lib::v2::func(); // calls v2
lib::func(); // calls v1 because it's inlined.
}
The list of edge cases and caveats just goes on and on - it never ends.
constexpr
The keyword constexpr has its own sets of nuances.
| Context | Explanation |
|---|---|
constexpr function | constexpr functions are implicitly inline. This means they can be defined in headers without causing multiple definition errors, and the compiler may insert their code at call sites. The inline behavior is automatic; you don't need to explicitly add the inline keyword. |
constexpr variable | constexpr variables are NOT implicitly inline. If you define a constexpr variable in a header and include it in multiple translation units, you'll get multiple definition errors at link time. To fix this, you MUST explicitly mark it as inline constexpr (C++17) to ensure only one instance exists across all translation units. |
Also you can't use constexpr on all types - it only works on constexpr compatible types.
constexpr std:::string wtf = "this doesn't work"; // this is going to error
constexpr std::string_view nice = "this works"; // this works because string_view is constexpr compatible.
And every version of C++ has different rules on how you can use constexpr.
Everything in this language feels hacked together - even the inheritance system.
In Java you can have an explicit interface keyword to create interfaces.
interface MyInterface {
public void method();
public void method2();
}
But in C++, you have to use the virtual keyword in every function, and then do this weird thing where you make each function equal to 0. And only then is considered to be an interface.
class MyClass {
public:
virtual void method() = 0;
virtual void method2() = 0;
virtual ~MyClass() = {}
};
class MyDerivedClass : public MyClass {
public:
void method() override {
std::cout << "MyDerivedClass::method" << std::endl;
}
void method2() override {
std::cout << "MyDerivedClass::method2" << std::endl;
}
}
This is a very unusual syntax. And strangely enough, when you overwrite functions in a derived class, the override keyword is purely optional.
class ConcreteClass : public MyInterface {
public:
void func1() override { // override keyword is optional
// implement something here
}
}
C++ is a crazy language, and learning it can be a daunting task for beginners.
Types
There is about 50 integer types, and the size of these types varies depending on which compiler you use and the target machine hardware.
| Type specifier | Equivalent type | Width in bits by data model |
|---|---|---|
int | int | at least 16 bits |
signed | int | at least 16 bits |
signed int | int | at least 16 bits |
Notice how it doesn't say that int IS 16 bits. It says that it's at least 16 bits, but it COULD also be 32 bits. And you have to know these rules about how in any given system, the size of a short will be less than or equal to an int, which will be less than or equal to a long.
short <= int <= long
Even the operating system which you use changes things. On Windows, long is 32 bits, even if it's 64-bit Windows. This is for backwards compatibility reasons. On 64-bit Linux, long is 64 bits.
The names are also just so verbose and ugly (wdym by long long???) I hate typing it - i wish we could all just agree to use something terse like i64 in the case where we want 64 bits. Again you could techinically create your own aliases:
using i64 = long long;
But it's ill advised.
And I hate how fixed width integer types have an _t suffix. Come on guys we are not in the 90s anymore.
uint8_t;
uint16_t;
uint32_t;
uint64_t;
And then you have to remember to use the correct one for the job. Beginners also find it weird how getting the size of standard library data structures return a size_t instead of an integer, which can also be of various sizes depending on the hardware. At some point you will inevitably run into the seven different character types and wonder what they all mean, and why a character would ever even need to be signed of unsigned like a number? And why an unsinged char is often used to represent bytes and binary data? Also what's a wchar ? And what's the difference between a std::string and a std::wstring? What are all the issues pertaining to that?
| Type | Size | Why it exists/Why use it |
|---|---|---|
char | 1 byte | Historical default. It's what C used for text, so C++ inherited it. You use it for normal ASCII text and strings (char*, std::string) because that's the convention everyone follows. Whether it's signed or unsigned is implementation-defined. |
signed char | 1 byte | When you need a tiny integer that can be negative. Range: -128 to 127. If you want to store small numbers to save memory, you explicitly use signed char to make it clear you're doing math, not storing characters. It's a distinct type from char. |
unsigned char | 1 byte | For raw byte manipulation because it has no trap representations. Range: 0-255. The C++ standard guarantees that unsinged char can hold any bit pattern safely. When reading binary files, network packets, or doing low-level memory operations, you need this guarantee. Also useful when you need small positive-only values. It's a distinct type from char. |
wchar_t | 2 or 4 bytes | Early attempt at internationalisation before Unicode won. Created when peopel realised ASCII wasn't enough for non-English text, but before UTF-8/16/32 became standard. Size is implementation-defined (2 bytes on Windows, 4 bytes on Linux/MacOS). Now it's stuck around for legacy compatibility, especially for Windows APIs. Used for wide strings (wchar_t*, std::wstring) because they are required for Unicode text. |
char8_t | 1 byte | To explicitly say "this is UTF-8". Regular char is ambiguous - is it ASCII? UTF-8? Latin-1? char8_t (C++20) removes all doubt and is guaranteed unsigned. Used for UTF-8 strings (char8_t*, std::u8string) because they are required for UTF-8 text. The compiler can also enforce UTF-8 correctness at compile time. |
char16_t | 2 bytes | For UTF-16 encoding. Windows APIs, Javascript, and Java use UTF-16 internally. You need this (C++11) when interfacing with those systems or when you specifcally want UTF-16. Used for UTF-16 strings ( char16_t*, std::u16string) although UTF-8 is more common now. |
char32_t | 4 bytes | When you want each variable to hold exactly one complete Unicode character. UTF-8 and UTF-16 use multiple bytes for some characters, making string indexing complex. UTF-32 (C++11) is simpler: character 5 is always at position 5. Used for UTF-32 strings (char32_t*, std::u32string). Trade-off: uses more memory. |
The stuff might make sense to you if you're an experienced developer, but if you're a beginner, it's incredibly confusing and it raises a million questions.
Different ways to DO THE SAME THING
Further more there's so many different ways to do the exact same thing in C++.
There are two different function syntaxes, the traditional one and the trailing return type syntax.
| Traditional Syntax | Trailing Return Type Syntax |
|---|---|
int func(int x) { return x + 1; } | auto func(int x) -> int { return x + 1; } |
int doThing() { // implementation } | auto doThing() -> int { // implementation } |
You can also get function like behavior using a struct.
// implementation of struct
struct {
[[nodiscard]] auto operator()(std::string_view s1, std::string_view s2) const -> std::string { return std::string{ s1 } + ", " + std::string{ s2 } + "!"; }
} doSomething;;
// usage.
std::string result = doSomething("Hello", "World"); // result is "Hello, World!"
And if you don't like parenthesis, you can use square brackets instead
struct {
[[nodiscard]] auto operator[](std::string_view s1, std::string_view s2) const -> std::string { return std::string{ s1 } + ", " + std::string{ s2 } + "!"; }
} doSomething;;
std::string result = doSomething["Hello", "World"]; // result is "Hello, World!"
Above two are both same thing.
const
const is another source of confusion.
Formatting and Style
Naming Conventions
Header Files
Namespaces
Compile Times
Modern C++
C/C++
C++ Edge Cases
Compilers and Build Systems
Installing a Library in C++
Package Managers
The Windows API
The Standard Library
New Features
Deprecated Features
The Fatigue Of Starting A New Project
Overall, there are a lot of issues with the standard library. But one of the key takeaway is that it's so low-level that in order to be productive, you inevitably end up writing your own wrappers and helpers around it to make it useful. But writing good generic wrappers and helpers requires the use of potentially complicated templates which can have daunting errors if you do something wrong. It's not realistic for beginners to know how to do this.
// template magic to get std::visit to work like Rust's match.
template<typename... Ts>
struct Overload : Ts... {
using Ts::operator()...;
};
template<typename Variant, typename... Handlers>
auto Match(Variant&& v, Handlers&&... handlers) {
return std::visit(
base::Overload{ std::forward<Handlers>(handlers)...},
std::forward<Variant>(v)
);
}
// wrapper around std::find_if to avoid manually writing .begin() and .end()
// and return a bool instead of an iterator
template<typename T, typename Predicate>
concept PredicateConcept = std::invocable<Predicate, T>;
template<typename T, typename Predicate>
requires PredicateConcept<T, Predicate>
bool contains_if(const std::vector<T>& vec, Predicate pred) {
return std::find_if(vec.begin(), vec.end(), pred) != vec.end();
}
The other option is to use third-party libraries, but as we covered earlier, that can be tricky too.
No matter what you choose, if you are a beginner, you are going to have a hard time. And it all just adds to the fatigue of starting a new project.
You have to pick which compiler to use, which IDE, which build system, which build system tools, which package manager, which style guide, how do you structure your project? Do you have standard library wrappers that you're going to carry over from your last project? Or are you going to use third party libraries? Which libraries are you going to use? You have to make all these critical infrastructure decisions at the beginning of your project that will haunt you for the rest of its lifetime.
Starting a new C++ project is incredibly dauting and you just can't hit the ground running. Most beginners seem to get stuck on just picking a UI library.
C++ GUIs
Creating a user interface in C++ is another labyrinth that you have to navigate. On Windows there are like 1- different official ways to make a UI. Each with varying degrees of support for C++.
