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. It's used for indicating that a variable's value is constant. It tells the compiler to prevent the programmer from modifying it. But counterintuitively, writing mutable const is completly valid C++ code.
class MyClass {
mutable const int *i
void setX(int *x) {
this->x = x;
}
}
MyClass myClass;
myClass.setX(new int(10));
std::cout << *myClass.x << std::endl; // 10
Note: this is only valid for class member variables. The
mutableapplies to the pointer variablei, allowing you to change where it points even inconstmember functions. But the thing it points to is stillconst int, so you can't modify the value through the pointer.
If you really want you can remove the constness of a variable by casting it away,
const_cast<>
but that's considered bad practice. Additionally many beginners are surprised to find out that when you make a member variable const, you can no longer assign your object to another object.
class MyClass {
public:
MyClass() : x(0) {}
private:
const int x; // const member variable imposes restrictions on the object
};
int main() {
MyClass a;
MyClass b;
b = a; // Compiler error! Assignment doesn'work.
b = std::move(a); // compiler error. move assignment doesn't work.
}
Another quirk of const is that it can go on either side of the type. But both do the exact same thing and there's no point to this.
const int i; // west const
int const i; // east const
There should be only one correct way to do this to avoid confusion because pointers can also be const.
int const * const i;
// const pointer to const int
int cnost * const var;
// var is a const pointer of const int
So it takes a bit of practice getting used to figuring out if the const keyword applies to the value of the type or the pointer. The general rule of thumb is read it from right to left. But this is stupid and unintuitive because most humans read from left to right.
Formatting and Style
C++ developers just can't seem to agree on how to format and style their code. Should you use snake_case or camelCase ? Should the opening curly brace go on a new line or not? Should the private section of a class be written at the top or the bottom? Should you prefix a variable based on the types or the context (int m_member vs int member)? What's the best way to split the code if it's too long? Should you put each parameter on a new line or just divide the function in the middle? Should you use the virtual keyword in derived functions or not? Should you use east const or west const? Should references and pointers stick to the type or the name?
int& ref = i;
int &ref = i;
int* ptr;
int *ptr;
Which extension should you use for the header files? .h or .hpp or .hh? And which one should you use for the implementation files? .cpp or .cc or .cxx, .c++, .C? Also tabs or spaces, and if so, how many?
It's like every single C++ codebase is an entirely different language.
You could spend your entire life working on, say, Unreal Engine C++ projects, and then struggle to do anything else because of all the custom macros and wrappers around it that you'd have to unlearn.
When I first set out to learn C++, I made it a goal of mine to study the GOogle C++ style guide. However, many seasoned developers cautioned me against it, explaining that the guide is designed for Google's aging codebases and tailored to address Google's specific challenges, many of which are no longer relevant for modern C++.
There's also the official C++ Core Guidelines, but historically it's always been very incomplete. As of recently, it seems like it's getting better. But nevertheless, most real life code bases just don't follow it. So you're stuck writing C++ in a completely different way depending on which codebase you're contributing to.
Naming Conventions
Moving forward, nearly everything in the C++ standard library that is important is poorly named.
In Java, you have simple, accurate, and self-descriptive names for the fundamental containers, like ArrayList, LinkedList, HashSet, and HashMap.
In C++, the most commonly used container is called vector. This is kind of like Java's ArrayList. But it's a horrible name because a vector is usually thought of as being a physics concept which represents a magnitude and a direction. Even the designer of the standard template library, Alex Stepanov, confessed that the name was a mistake.
Additionally, if you want to use a hash set or hash map in C++, you might mistakenly reach for std::set and std::map. But these are not what you think they are. They are implemented as balanced binary trees and give you logarithmic lookup time complexity as opposed to constant time complexity, which is what you would have expected based on the name. If you want constnat lookup time complexity, then you have to use unordered_set and unordered_map.
| Java | C++ |
|---|---|
ArrayList | std::vector |
LinkedList | std::list |
HashSet | std::unordered_set |
HashMap | std::unordered_map |
But honestly you shouldn't be using these either, and I'll explain why in just a moment.
Even the methods of these fundamental data structures are poorly named. Many containers, for example, vector, have a vector.empty() function. It's hard to tell at first glance if this function is a question which checks if the container is empty or if it's a verb which empties the container. A better name would've been vector.is_empty().
The standard library is filled to the brim with confusing incomprehensible function names like std::stoi and std::stol and std::stoll and std::stof, std::stod and std::stold etc.
And I know that some of these were probably inspired by their C counterparts and they are just abbreviations of types. But cryptic names like this can be very intimidating to beginners, and it makes everything seems more complicated than it needs to be.
| C++ Function | C Equivalent | What it does |
|---|---|---|
std::stoi | atoi | Converts a string to an integer |
std::stol | atol | Converts a string to a long integer |
std::stoll | atoll | Converts a string to a long long integer |
std::stof | atof | Converts a string to a float |
std::stod | atod | Converts a string to a double |
std::stold | atold | Converts a string to a long double |
Even the most commonly used terms, idioms and best practices in C++ are terribly named. For example, RAII, which stands for Resource Acquisition Is Initialization. This is a super important idiom in C++, but the name makes it so much more confusing than it actually is. It just means that when an object goes out of scope, its resources should be freed automatically. So make sure you put the resouce releasing logic in the destructor.
class MyClass {
public:
MyClass() {
// acquire resource
}
~MyClass() {
// release resource
}
};
A much better name would have been Scope Bound Resource Management. Or CADRe which stands for Constructor Acquires, Destructor Releases. Too bad neither of them caught on.
Another example of bad naming is the CRTP idiom, which stands for Curiously Recurring Template Pattern. The name has very little to do with what it means. Curiously Recurring just refers to the fact that the guy who coined the term, James O. Coplien, stumbled upon this pattern on various occasions and was intrigued by it. Hence the name Curiously Recurring. This has nothing to do with what the pattern actually does. It only describes his impression of the pattern, which is useless in helping people understand what the pattern is. It should've been called Static Polymorphism Template Pattern (SPTP) or Compile Time Inheritance (CTI). It would've been far more self-descriptive and helpful to beginners.
C++ also has something called std::monostate, which is intended to be used inside variants to represent an empty state. The name monostate is the most pretentious deliberately abstruse thing I've ever heard. It's absolutely wild to me that they chose that name. They should've called it std::empty or std::blank or std::void or sth.
Deducing This
There's also a feature called Deducing this in C++23. Basically when we call an object's member function, under the hood, the object is implicitly passed to the member function, even though you don't see it in the parameter list. "Deducing this" lets you make the parameter explicit. This has various advantages that I won't cover. My main gripe is that it should have been called something more self-descriptive like Explicit Object Parameter.
// without deducing this
class MyClass {
public:
void doSomething() { // object is passed implicitly
// do something with this
}
};
// with deducing this
class MyClass {
public:
void doSomething(MyClass& self) { // object is passed explicitly
// do something with this
}
};
MyClass myClass;
myClass.doSomething();
And the remove function std::remove in the standard library doesn't even remove anything. It just moves the elements to the end of the container.
As C++ evolves, the naming conventions only get more and more confusing. Many new features, which are meant to be improvements of old deprecated ones, have less than ideal names. The standard library has a mutex wrapper called std::lock_guard, but it's not able to lock multiple mutexes.
std::lock_guardA mutex wrapper that provides a convenient and safe way to manage mutexes using the Resource Acquisition Is Initialization (RAII) pattern. But there are some issues
- Inability to lock multiple mutexes
- Deadlock Risk
- Verbose
There was a proposal to extend lock_guard to include this feature, but that was found to break backwards compatibility. So they made a new mutex wrapper called std::scoped_lock.
Developers have been critical about the choice of this name. It's hard to remember which one is the old version and which one is the new version based on the name alone. Some developers argue that it should have been called lock_guard2 to make it more explicit that it's the newer version.
In the same vein, std::thread and std::function also have issues, but because they can't break backwards compatibility, they made jthread and copyable_function which contain the fixes. Again the chosen names completely failed to communicate that these are the newer improved versions. They probably should have been called thread2 and function2, respectively.
| Old | Issues | New | What's fixed | What it should**'**ve been |
|---|---|---|---|---|
std::thread | No automatic join, can terminate program when thread is still running | std::jthread | Auto-join with stop token support | std::thread2 |
std::function | const correctness bug | std::copyable_function | Fiexes const correctness bug | std::function2 |
std::lock_guard | No ability to lock multiple mutexes | std::scoped_lock | Can lock multiple mutexes | std::lock_guard2 |
Header Files
The only thing more nauseating than the naming convention however, is dealing with header files. Header files are just awful.
Pretty much everytime you create a C++ file, you also need to create a corresponding header file, and you need to make sure that they're synchronised at all times. This is a blatant violation of the "Don't Repeat Yourself" principle. If you change the parameters of a function in the cpp file, you also need to make sure to update the contents in the corresponding header file to match.
And god forbid, if you use the wrong paremeter names in the header file, it'll comile just fine without letting you know:
// header file - this would compile just fine
void process(int width, int height) {
// implementation
}
// cpp file
void process(int height, int width) {
// implementation
}
If you create a class that requires a constructor, you end up writing the class name four times.
// twice in the header file
class MyClass {
MyClass();
}
// cpp file - two more times in the cpp file where you define the constructor.
MyClass::MyClass() {
// implementation
}
Compare this to almost any other language like Java, for example, where the exact same thing is half the amount of work. The other problem with header files is that they bloat up your project directory. An application could have only required 100 files to complete, now requires 200 files in total because of the corresponding header files.
This also means that you have twice the amount of files to check in pull requests and code reviews. It just adds so unnecessary mental fatigue to dealing with this language. And there's so many different ways of organising your project structure.
Some devs keep their header and cpp files in the same folder. This is typically used in applications. Other developers keep them into different folders. This is typically used in libraries.
Perhaps the worst thing about header files is that when you include them, the compiler just stupidly copies and pastes the content of the file into the line where you've included it. This means that if you've included something more than once due to a transitive dependency, you now have the same types and functions defined more than once. This breaks the One Definition Rule and gives you a compiler error.
To fix this, you have to tell thr compiler not to include something again if it's already been included. You do this with header guards, whic you have to put inside every single header file in your entire codebase. It's exhausting. And developers online argue incessantly about which technique to use because there are at least two ways to do it.
The first is with include guards which are more verbose because it requires three separate preprocessor directives and necessitates that you create and maintain a unique macro name.
#ifndef HEADER_NAME_H
#define HEADER_NAME_H
// header content goes here
#endif
Ususally this name is based on the file name. Don't forget to update it if you change the file name.
The second is with pragma once which is more concise, but for some reason this is not recognised by the C++ standard. So it's not the officially agreed upon way.
#pragma once
// header content goes here
And because it's not in the standard, some people say that you should never use it, wherease other say that you should use both together at the same time.
All these incessant arguing ignores the fact that this issue shouldn't even exist in the first place. The compiler should just know to include a file only once. And if you don't like that, there should be a way to opt out and not the other way around.
Developers sometimes try to justify the existence of header files by arguing that they represent the public interface to your code. However classes which declared in headers have their private variables and functions exposed there too.
class MyClass {
public:
void execute();
private:
ImplementationDetail detail;
};
This is a leaky abstraction. Private implementation details don't belong in a public interface. But since they exist there, you have to include those dependencies in the header file. This means that anyone who includes your header file now by extension also ends up transitively including the files related to the implementation details of the class.
To add insult to injury, if you change a private member variable, you have to recompile everything that includes that header. Even if that change is purely internal and semantically irrelevant to client code.
Pimpl
There are some solutions to this, but not really. There is a design pattern called pimpl (plmpl for compile-time encapsulation in C++, pointer to implementation) that can be used to hide the implementation details entirely inside the cpp file. But it makes things more complicated and it comes with a performance penalty.
And honestly, in a well-designed language, private variables like this should be hidden by default without all this unncessary work.
C++ in general has a very weird ways of hiding implementation details. If you want to isolate your code from the outside, you can use an anonymous namespace, or you can make your function static as we discussed earlier, or you can do both at the same time.
namespace {
void helper() {
// implementation
}
}
static void helper() {
// implementation
}
namespace {
static void helper() {
// implementation
}
}
And as I've stated before, I would have preferred a private keyword for this purpose instead.
You might also see people try to hide their implementation details by putting them in a detail or implementation folder and corresponding namespace. But there's no built-in mechanism in C++ to prevent others from accessing the code in these folders. Anyone can easily include these implementation files and use them anyways.
The user has to know which files are ok to include and which are not, usually by reading documentation written by the creator of the library, which may or may not exist.
Another major flaw of C++'s header files system is how much it pollutes the autocomplete suggestions. As I mention earlier, the #include preprocessor directive is just a glorified copy and paste operation. So it dumps all the types, functions, and other symbols from the header file directly into your code, which in turn pollutes your autocomplete suggestions with functions and types from all over your codebase.
Consider the situation where you include a header that include 10 more headers, and each of those headers include 10 more headers, and so on and so forth. You end up with a million symbols from the entire dependency tree flooding your autocomplete and make it useless. I can hardly rely on using tab to autocomplete my code because it's replete with irrelevant suggestions.
And this is why developers hate it when noobs #include <windows.h>. It imprts like another million symbols into your project and macros make everything even worse.
Macros
Macros in C++ are basically search and replace commands that happens before your code is actually compiled. They are executed by the preprocessor.
If you write #define SIZE 100 you're telling the compiler to replace every instance of SIZE with 100.
Macros don't respect the type system or scoping rules, and they have no semantic awareness, and they operate indiscriminately across all code which is downstream in the include chain. This means that if you _include a header with macros, those macros can hijack your code because those search and replace commands are now active in your code, whether you like it or not. It's like inviting someone into your house and they secretly start rearranging your furniture without telling you.
Imagine that you have a function SendMessage and then you #include <windows.h>. windows.h has a macro for SendMessage which ends up overwriting your SendMessage function.
Or imagine if someone write a macro to make private public, which is sometimes used naively for writing tests.
#define private public;
class MySecureData {
private: // surprise! this is now public due to the macro
int sensitve_data;
}
Now all your private variables and functions are exposed.
Given the fact that the header inclusion system dumps all the symbols directly into your code, the probability of there being name clashes is much higher than in modern languages that have proper module support. This is something that C++ inherited from C. And has led to the creation of another problematic feature which is namespaces.
Namespaces
If you're moving to C++ from C, namespace look like a fantastic feature, because the solution to name classing in C was to prefix your functions with your project name.
// C
LIB_doStuff();
// C++
lib::doStuff();
// other languages
doStuff();
But in C++, you use namespaces instead. Wonderful. But if you move from other languages to C this is a serious downgrade (C#, Go, TS, JS, Java etc. ), this is a serious downgrade because namespaces make the code look hideous.
Modern languages don't force you to write namespace prefixes everywhere. In modern languages, name clashing occurs far less frequently because they don't use header files. So you deal with naming ambiguities as they arise.
And yes, i know that you can use the using keyword to omit namespace prefixes in C++, but then you're back to square one.
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++.
