Modern C++ Best Practices
Modern C++ Best Practices
Adopt habits that improve clarity, safety, and maintainability in everyday code.
Modern C++ Best Practices
Adopt habits that improve clarity, safety, and maintainability in everyday code.
Simple code is easier to debug, review, and extend.
int score(const std::vector<int>& values) {
int total = 0;
for (auto v : values) {
total += v > 0 ? v : 0;
}
return total;
}
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.
const by default.std::vector by default for dynamic lists.std::string_view for non-owning text parameters when lifetime is safe.std::unique_ptr for exclusive ownership.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.
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.
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.
std::span and std::string_view for read-only borrowed data when lifetimes are clear.[[nodiscard]].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.
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.
std::optional when "no result" is normal.std::expected when callers should process structured error information.std::optional<T> when the caller only needs to know whether a value exists.std::expected<T, E> when the caller needs a useful reason for failure.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;
}
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.
std::vector<int> values{1, 2, 3};
for (int value : values) {
// prefer clear intent and small, testable steps
}
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;
}