Ten C++ habits that quietly remove whole classes of bugs
None of these are clever. That's the point: each one is a habit that removes a category of mistake rather than a single bug, and none of them cost run-time performance. Everything here compiles with GCC 13, Clang 17 and MSVC 19.38 or newer in C++20 mode.
1. Let destructors do the clean-up (RAII)
If you write delete, fclose, unlock or close by hand, an early return or an exception will eventually skip it. Wrap the resource in an object whose destructor releases it, then stop thinking about it.
#include <cstdio>
#include <memory>
using file_ptr = std::unique_ptr<std::FILE, decltype([](std::FILE* f) { if (f) std::fclose(f); })>;
file_ptr open(const char* path, const char* mode) {
return file_ptr{std::fopen(path, mode)};
}
void write_report() {
auto f = open("report.txt", "w");
if (!f) throw std::runtime_error("cannot open report.txt");
std::fputs("hello\n", f.get());
} // closed here, on every path
The same shape covers mutexes (std::scoped_lock), memory (std::unique_ptr, std::vector) and anything you can describe as "acquire now, release later".
2. Make everything const unless it can't be
A const local can't be modified by a stray line 300 lines later; a const member function can't mutate state. Reading code becomes easier because the reader knows what won't change. Start with const and remove it when the compiler complains.
const auto users = load_users(); // won't change again
const auto total = std::ranges::fold_left(users, 0, [](int n, const User& u) { return n + u.logins; });
3. Prefer values to pointers
Ownership questions disappear when there is nothing to own. Return objects by value (the compiler elides the copy), keep members as values, and reach for std::unique_ptr only for polymorphism or genuinely optional, heavy state. std::shared_ptr is the last resort, not the default.
4. Pass std::span and std::string_view, not pointer-plus-length
double mean(std::span<const double> xs) {
if (xs.empty()) return 0.0;
return std::accumulate(xs.begin(), xs.end(), 0.0) / static_cast<double>(xs.size());
}
std::vector<double> v{1, 2, 3};
double arr[] = {4, 5, 6};
mean(v); mean(arr); // both work, no size argument to get wrong
Remember neither type owns its data: don't return a span or string_view that refers to a local. (Thanks to Anna in the comments for pointing out that this deserved saying explicitly; that's what version 2 of this post adds.)
5. Brace-initialise, and initialise members in the class
Braces refuse narrowing conversions and can't be parsed as a function declaration. Default member initialisers mean a forgotten constructor can't leave a field uninitialised.
struct Config {
int retries {3};
bool verbose {false};
std::string name{"default"};
};
Config c{}; // fully initialised
int x{2.5}; // error: narrowing
6. Use enum class
Scoped enums don't convert to int silently and don't leak their names into the enclosing scope, so Colour::Red and Status::Red can coexist and if (colour == 2) is a compile error rather than a surprise.
7. Return std::optional or std::expected instead of magic values
std::optional<User> find_user(std::string_view name);
std::expected<Image, std::string> decode(std::span<const std::byte> bytes); // C++23
if (auto u = find_user("mira")) greet(*u);
auto img = decode(bytes);
if (!img) log(img.error());
The absence or failure is part of the type, so callers can't forget to check -1 or nullptr.
8. Reach for algorithms and ranges before writing a loop
A named algorithm says what it does; a raw loop has to be read. Off-by-one errors live in loops.
auto active = users | std::views::filter(&User::active) | std::views::transform(&User::email);
for (const auto& email : active) send(email);
std::ranges::sort(users, {}, &User::name); // sort by projection, no comparator lambda
9. Turn on warnings and sanitisers, and treat warnings as errors in CI
# CMake
target_compile_options(app PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>)
# Debug builds
-fsanitize=address,undefined
AddressSanitizer and UndefinedBehaviorSanitizer catch the bugs that would otherwise only show up on a customer's machine. Run your tests under them.
10. Test at compile time where you can
static_assert and constexpr functions turn a class of run-time tests into compile errors. Constants, lookup tables, parsers of fixed formats and unit conversions are all candidates.
constexpr int checksum(std::string_view s) {
int n = 0; for (char c : s) n = (n * 31 + c) % 65521; return n;
}
static_assert(checksum("phookit") == 8394, "checksum algorithm changed");
Further reading
- C++ Core Guidelines (Stroustrup & Sutter, open source, CC-BY)
- cppreference.com for every facility mentioned above
Comments 2
Log in or register to join the conversation.
egsw
Show 1 reply
Loading…