Let the Standard Library Do the Work
The single biggest difference between C++ written in 1998 and C++ written today is how much of it there isn't. Modern C++ replaces whole categories of hand-written, error-prone code with standard types that already work, and the most reliable way to write good C++ is to reach for those first.
Concretely: std::vector instead of new[] and a size variable. std::string instead of a char buffer and strcpy. std::unique_ptr instead of a raw owning pointer and a delete you have to remember on every exit path. std::sort instead of a bubble sort you wrote at 2 a.m. Every one of these substitutions removes code, removes bugs, and is at least as fast as what it replaced.
This is what the Rule of Zero from the constructors lesson is really about. If every member of your class manages itself, the compiler-generated destructor, copy operations and move operations are all correct, and you write none of them. The hard code exists once, inside the standard library, where it has been reviewed by more people than will ever read yours.
The corollary is that when you find yourself writing new, delete, a raw array, or a manual loop that walks a container looking for something, pause and ask what standard facility already does it. Nine times out of ten there is one, and using it makes the code shorter and the intent clearer — std::any_of says what it is doing in a way a hand-written loop with a flag variable never quite manages.
The one situation where writing it yourself is right is when you are learning. Implement a linked list, a stack and a deep-copying class once, so that you know what the library is doing for you. Then stop.
std::vectorfor run-time-sized collections;std::arraywhen the size is a compile-time constantstd::stringandstd::string_viewfor text — neverchar[]andstrcpystd::unique_ptrfor exclusive ownership;std::shared_ptronly when ownership is genuinely shared<algorithm>and<numeric>instead of hand-written search, sort, count and sum loopsstd::optional(C++17) for a value that may legitimately be absent, instead of a magic-1- Range-based
forinstead of index arithmetic, whenever you do not need the index std::filesystem(C++17) for paths and file existence rather than platform-specific calls
- Use the newest standard your toolchain supports, and pin it explicitly with
-std=c++17or-std=c++20. Relying on the compiler's default is how a program that builds on your laptop fails on somebody else's.
const by Default, and the Smallest Possible Scope
Two habits do more for C++ code quality than any amount of cleverness: mark things const unless they need to change, and declare every variable in the narrowest scope that needs it.
const-correctness is a chain. Mark a local variable const and the compiler catches an accidental assignment. Mark a member function const and it can be called on objects passed by const reference — which is how most objects get passed, so a missing const on a getter makes it useless in half your codebase. Take parameters as const T& and the caller can see from the signature that their object is safe, and can pass temporaries.
The reason this compounds is that const is contagious in the right direction: once a function takes const T&, everything it calls on that object must also be const. Adding const to a codebase later is famously painful, whereas writing it from the start costs nothing. The rule is easy: write const, remove it only when the compiler complains.
The scope habit is equally cheap. A variable declared just before its first use, and initialised on the same line, cannot be read before it has a value, cannot be accidentally reused two hundred lines later, and tells the reader immediately what it is for. C's habit of declaring everything at the top of a function was a language limitation, not a style, and C++ has never had it. The C++17 if (init; condition) form takes this further by scoping a variable to a single branch.
A related point on auto: use it when the type is long and mechanical, such as an iterator or a lambda, and spell the type out when it is short and meaningful. auto also drops references and const unless you ask, so auto& x = v[0] refers to the element while auto x = v[0] copies it — a distinction that quietly matters inside loops.
#include <iostream>
#include <map>
#include <string>
#include <vector>
// const& parameter, const method, const local — the whole chain
class Roster {
std::vector<std::string> names_;
public:
void add(const std::string& name) { names_.push_back(name); }
std::size_t size() const { return names_.size(); } // const!
const std::vector<std::string>& names() const { return names_; }
};
void printAll(const Roster& r) { // const ref: no copy, no modification
for (const auto& n : r.names()) { // const auto&: no copy per element
std::cout << n << '\n';
}
std::cout << r.size() << '\n'; // works only because size() is const
}
int main() {
Roster r;
r.add("Ananya");
r.add("Rahul");
printAll(r);
const double TAX_RATE = 0.18; // will never change: say so
constexpr int MAX_STUDENTS = 60; // known at compile time: better still
std::map<std::string, int> marks = {{"Ananya", 91}};
// Declared where it is used, scoped to the branch that needs it
if (auto it = marks.find("Ananya"); it != marks.end()) {
const int score = it->second;
std::cout << score * TAX_RATE << ' ' << MAX_STUDENTS << '\n';
}
// `it` and `score` do not exist here, so they cannot be misused
} - Prefer
enum classover a plainenum. A scoped enum does not leak its names into the surrounding scope and does not silently convert toint, soColour::Redcannot be compared withStatus::Activeby accident. The old form still compiles and still causes those bugs.
Performance: Measure, Then Fix What Is Actually Slow
C++ attracts people who care about speed, and that enthusiasm often gets spent in the wrong places. The reliable process is unglamorous: write the clearest correct code first, measure it, and optimise only what the measurement points at.
The reason is that intuition about performance is poor. Most of a program's run time is usually spent in a small fraction of its code, and that fraction is rarely where you would guess. Time spent hand-optimising a function that runs twice is time wasted, and it usually makes the code harder to read — which makes the real problem harder to find later.
That said, a few habits are free and worth applying everywhere because they cost nothing in readability. Pass big objects by const reference rather than by value. Call reserve when you know how many elements are coming. Use '\n' rather than std::endl in loops. Prefer ++it to it++. Use std::move when handing over an object you are finished with. None of these is a micro-optimisation; they are simply the default way to write the same code.
When you do need to look deeper, the costs that dominate real C++ programs are almost always the same three: allocations (every new, every vector growth, every string built in a loop), copies (a by-value parameter, a auto that should have been auto&), and cache misses (jumping around memory instead of walking it in order). A std::vector usually beats a std::list for this last reason alone, even where the theory says otherwise.
And measure with optimisation on. Timing a debug build tells you almost nothing about the release build your users will run — -O2 changes the picture completely, and a benchmark compiled without it can point you at entirely the wrong function.
- Write it clearly first. Correct and readable beats fast and wrong.
- Measure with a profiler and an
-O2build before changing anything. - Pass large objects by
constreference; return by value and let copy elision work. reserve()when the final size is known; it turns several allocations into one.- Prefer contiguous containers —
std::vector— for anything you iterate over often. - Use
std::movefor objects you are finished with, and mark move operationsnoexcept. - Prefer stack objects to heap objects; heap allocation is one of the more expensive things a program does.
- Do not sacrifice clarity for a gain you have not measured.
- For competitive programming specifically, the two lines
std::ios::sync_with_stdio(false);andstd::cin.tie(nullptr);plus using'\n'instead ofstd::endlsolve most input/output time limits. They are the wrong default for interactive programs, so keep them to that setting.
Habits That Catch Bugs Before You Do
C++ will let you write undefined behaviour without a murmur, so the practical response is to enlist the tools that do notice. All of these cost a flag or a few minutes of setup and repay it immediately.
Turn on warnings and take them seriously. -Wall -Wextra catches uninitialised reads, signed/unsigned comparisons, functions that can end without returning, mis-ordered member initialiser lists, and unused variables that usually mean you edited something and forgot a line. Every one of those is a real bug class. Once your code is clean, add -Werror so warnings cannot accumulate.
Use the sanitizers while developing. -fsanitize=address catches out-of-bounds access, use-after-free and leaks; -fsanitize=undefined catches several kinds of undefined behaviour. They make the program slower, which is exactly the right trade during development, and they turn silent corruption into a message naming the file and line. Build with them for testing, without them for release.
Initialise every variable where you declare it, prefer at() over [] for any index that came from outside your control, and never write new or delete in ordinary application code. Between them these three habits eliminate most of the memory bugs a beginner writes.
Beyond the compiler, a static analyser such as clang-tidy or cppcheck reads your code and reports suspicious patterns the compiler does not, and a formatter such as clang-format ends every argument about layout by making it automatic. Both are worth adding to a project on day one, when there is nothing to fix, rather than on day two hundred when there is a lot.
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
enum class Status { Active, Suspended, Closed }; // scoped: no silent int conversion
int main() {
// Initialise at the point of declaration, always
int total = 0;
double average = 0.0;
std::vector<int> marks;
marks.reserve(3);
marks.push_back(91);
marks.push_back(65);
marks.push_back(78);
for (int m : marks) total += m;
average = static_cast<double>(total) / marks.size();
// at() when the index is not under your control
std::size_t index = 5;
try {
std::cout << marks.at(index) << '\n';
} catch (const std::out_of_range& e) {
std::cerr << "bad index: " << e.what() << '\n';
}
// Ownership is explicit; no new, no delete
auto note = std::make_unique<std::string>("term 1");
Status s = Status::Active;
// if (s == 0) { } // will not compile — and that is the point
std::cout << average << ' ' << *note << ' '
<< (s == Status::Active) << '\n'; // 78 term 1 1
} - A build command worth keeping in your shell history while learning:
g++ -std=c++17 -Wall -Wextra -g -fsanitize=address,undefined -o app main.cpp. Nearly every memory bug in this course becomes a clear error message under it.
Writing Code Other People Can Read
Most of the code you write will be read far more often than it is written, frequently by you six months later with no memory of what you were thinking. Readability is not politeness; it is how a project stays changeable.
The highest-value habit is naming. A function should be named after what it does, not how — calculateGst, not process. A variable should be named after what it holds; single letters are fine for loop indices and nothing else. A bool should be named for the true case, so that if (isEnrolled) reads as a sentence and if (!notDisabled) never gets written.
The second is size. A function that does one thing fits on a screen and can be named honestly. When you find yourself writing a comment inside a function that says "now validate the input", that block wants to be a function called validateInput — after which the comment is unnecessary, the logic is testable on its own, and the outer function reads as a summary.
Comments should explain why, not what. // increment i adds nothing. // the exam board rounds 32.5 up, unlike std::round is worth its space forever, because it records a decision that the code cannot express. If a comment is needed to explain what a line does, the line usually wants rewriting instead.
Finally, header hygiene, since it affects everyone who includes your file. Put #pragma once at the top of every header. Never write using namespace std; in a header — it forces the whole standard library into the scope of every file that includes it, and that file's author has no say in the matter. Include what you use, and prefer forward declarations in headers to keep compile times sane.
- Name functions after their purpose; name booleans for the true case
- One job per function, one responsibility per class
- Comments explain why a decision was made, not what the code does
#pragma oncein every header; nousing namespacein any header- Declare variables at first use, initialised, in the smallest useful scope
- Prefer returning a value over filling an output parameter
- Be consistent — an existing project's style beats your preferred style every time
- If you want a single reference beyond this course, the C++ Core Guidelines, maintained by Bjarne Stroustrup and Herb Sutter, collect this kind of advice with the reasoning attached. Read a section at a time rather than end to end; most of it makes far more sense once you have hit the problem it describes.
