Clean Code на C++: принципы и примеры на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему чистый код важен для C++

C++ — мощный и гибкий язык, который позволяет писать как высокоэффективный системный код, так и сложные прикладные решения. Однако эта гибкость часто оборачивается хаосом: перегруженные функции, неявные преобразования, ручное управление памятью и макросы могут превратить проект в «болото» за несколько месяцев. Концепция Clean Code (чистый код), популяризированная Робертом Мартином, применима к C++ в полной мере, но с учетом специфики языка.

В этой статье мы разберем ключевые принципы чистого кода на C++ и покажем их применение на реальных примерах. Вы узнаете, как писать код, который легко читать, тестировать и поддерживать.

1. Именование и структура: основа читаемости

1.1. Осмысленные имена переменных и функций

Первое правило чистого кода — имя должно раскрывать намерение. В C++ это особенно важно из-за наличия перегрузок и шаблонов.

Плохой пример:

int d; // elapsed time in days
void proc(int a, int b); // что такое a и b?

Хороший пример:

int elapsedTimeInDays;
void processData(const std::vector<int>& inputData, int maxIterations);

1.2. Классы и пространства имен

Используйте пространства имен для логической группировки. Избегайте глобальных переменных — это антипаттерн.

namespace network {
    class TcpConnection {
    public:
        void connect(const std::string& host, int port);
        void send(const std::vector<char>& data);
    private:
        int socket_;
    };
} // namespace network

2. Функции: маленькие и с одной ответственностью

2.1. Принцип единственной ответственности (SRP)

Функция должна делать только одно дело. Если функция делает больше одного действия — разбивайте её.

Плохой пример:

void processOrder(Order& order) {
    // валидация
    if (order.total < 0) throw std::invalid_argument("Negative total");
    // сохранение в БД
    database.save(order);
    // отправка email
    emailService.send(order.customerEmail, "Order confirmed");
}

Хороший пример:

bool validateOrder(const Order& order);
void saveOrderToDatabase(const Order& order);
void sendConfirmationEmail(const Order& order);

void processOrder(Order& order) {
    if (!validateOrder(order)) return;
    saveOrderToDatabase(order);
    sendConfirmationEmail(order);
}

2.2. Избегайте побочных эффектов

Функция не должна изменять глобальное состояние без явной необходимости. Используйте константные ссылки и возвращайте значения, а не изменяйте аргументы.

// Плохо: изменяет входной вектор
void addPrefix(std::vector<std::string>& words, const std::string& prefix) {
    for (auto& word : words) {
        word = prefix + word;
    }
}

// Хорошо: возвращает новый вектор
std::vector<std::string> addPrefix(const std::vector<std::string>& words, const std::string& prefix) {
    std::vector<std::string> result;
    result.reserve(words.size());
    for (const auto& word : words) {
        result.push_back(prefix + word);
    }
    return result;
}

3. Управление памятью и RAII

3.1. Используйте умные указатели

В современном C++ (C++11 и новее) ручное управление памятью через new/delete считается устаревшим. Используйте std::unique_ptr и std::shared_ptr.

// Плохо: утечка памяти возможна
void process() {
    Resource* res = new Resource();
    // ... если здесь исключение, память утечет
    delete res;
}

// Хорошо: автоматическое освобождение
void process() {
    auto res = std::make_unique<Resource>();
    // ... даже при исключении память освободится
}

3.2. RAII — Resource Acquisition Is Initialization

Это ключевая идиома C++. Ресурс (файл, сокет, мьютекс) захватывается в конструкторе и освобождается в деструкторе.

class FileHandler {
public:
    explicit FileHandler(const std::string& filename)
        : file_(fopen(filename.c_str(), "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    ~FileHandler() {
        if (file_) fclose(file_);
    }
    // запрещаем копирование
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
private:
    FILE* file_;
};

4. Обработка ошибок и исключения

4.1. Исключения вместо кодов возврата

В C++ принято использовать исключения для обработки ошибок, а не возвращать коды ошибок (как в C). Это делает поток выполнения более чистым.

// Плохо: смешивание логики и обработки ошибок
int divide(int a, int b, int& result) {
    if (b == 0) return -1; // код ошибки
    result = a / b;
    return 0; // успех
}

// Хорошо: исключение
int divide(int a, int b) {
    if (b == 0) throw std::invalid_argument("Division by zero");
    return a / b;
}

4.2. Не злоупотребляйте исключениями

Исключения — для исключительных ситуаций, не используйте их для управления потоком. Для ожидаемых ошибок (например, не найден файл) используйте std::optional или std::expected (C++23).

std::optional<std::string> readFileContent(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) return std::nullopt;
    std::string content((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());
    return content;
}

5. Комментарии: меньше, но лучше

Чистый код самодокументируем. Комментарии должны объяснять почему код написан так, а не что он делает.

// Плохо: комментарий дублирует код
// Увеличиваем счетчик на 1
counter++;

// Хорошо:                

Блоги

Рекомендаций по книгам