Modern C++ Best Practices

Modern C++ Best Practices

Adopt habits that improve clarity, safety, and maintainability in everyday code.

Modern C++ Best Practices

Prefer clarity over cleverness

Simple code is easier to debug, review, and extend.

Before: compact but harder to reason about

int score(const std::vector<int>& values) {
	int total = 0;
	for (auto v : values) {
		total += v > 0 ? v : 0;
	}
	return total;
}

After: intent is obvious

bool is_positive(int value) {
	return value > 0;
}

int sum_positive(const std::vector<int>& values) {
	int total = 0;
	for (int value : values) {
		if (is_positive(value)) {
			total += value;
		}
	}
	return total;
}

The second version is only slightly longer, but it exposes the rule directly and gives you a natural place for tests.

Good defaults

These defaults reduce decision fatigue. You can deviate when you have a reason, but starting from well-understood types and idioms keeps code reviews simpler.

Design guidance

Example: represent valid state directly

struct ConnectionConfig {
	std::string host;
	std::uint16_t port;
};

class Client {
public:
	explicit Client(ConnectionConfig config) : config_(std::move(config)) {}

private:
	ConnectionConfig config_;
};

Prefer a validated configuration object over a class that can be default-constructed into a broken state and fixed later through multiple setter calls.

Tooling guidance

Minimum local build command

g++ -std=c++23 -Wall -Wextra -Wpedantic -g -fsanitize=address,undefined src/main.cpp -o build/app

Treat warnings and sanitizers as part of normal development, not as a cleanup phase at the end.

API design guidance

Prefer returned values over output parameters

std::string build_label(std::string_view name, int id) {
	return std::format("{}-{}", name, id);
}

This is easier to use and test than forcing callers to allocate an output string and pass it in by non-const reference.

Use borrowed views only when lifetime is obvious

void log_names(std::span<const std::string> names);
void greet(std::string_view name);

Borrowed views are excellent API tools, but they are a bad fit when the callee needs to store the data past the call.

Error-handling guidance

Quick choice rule

std::expected<int, std::string> parse_port(std::string_view text) {
	int value = 0;
	auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
	if (ec != std::errc{} || ptr != text.data() + text.size()) {
		return std::unexpected("port must be a decimal number");
	}
	return value;
}

Modern feature adoption

Adopt features that improve readability and correctness first: concepts, std::span, std::jthread, std::format, and std::expected typically pay off faster than large architecture rewrites.

Checklist for day-to-day code reviews

Example in practice

std::vector<int> values{1, 2, 3};
for (int value : values) {
    // prefer clear intent and small, testable steps
}

Try this variation

Refactor the example into a named helper with a single responsibility. If the logic gets easier to test, the design probably improved.

bool is_even(int value) {
    return value % 2 == 0;
}

int main() {
    return is_even(4) ? 0 : 1;
}