← Назад к вопросам

Для чего нужен throw без аргументов?

2.0 Middle🔥 61 комментариев
#Исключения и обработка ошибок

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

throw без аргументов — Пробросить текущее исключение

throw; без аргументов — это оператор, который пробрасывает текущее (обрабатываемое в данный момент) исключение дальше вверх по стеку вызовов. Это отличается от выброса нового исключения.

Основное различие

// throw; — пробросить ТЕКУЩЕЕ исключение
try {
    dangerous_operation();
} catch (const std::exception& e) {
    std::cout << "Logging: " << e.what() << std::endl;
    throw;  // Пробросим то же самое исключение дальше
}

// throw e; — выбросить новое исключение (КОПИЮ)
try {
    dangerous_operation();
} catch (const std::exception& e) {
    std::cout << "Logging: " << e.what() << std::endl;
    throw e;  // Выбросим копию e
}

// throw std::runtime_error(...); — выбросить новое исключение
try {
    dangerous_operation();
} catch (const std::exception& e) {
    std::cout << "Logging: " << e.what() << std::endl;
    throw std::runtime_error("Processing failed");
}

Почему throw; лучше чем throw e;

Проблема с throw e:

class MyException : public std::exception {
    std::string message;
public:
    MyException(const std::string& msg) : message(msg) {}
    const char* what() const noexcept override {
        return message.c_str();
    }
};

try {
    throw MyException("Original error");
} catch (const MyException& e) {
    std::cout << "Type: " << typeid(e).name() << std::endl;
    throw e;  // ПРОБЛЕМА: Копируется как std::exception!
             // Потеряется type информация в наследнике
}

try {
    throw MyException("Original error");
} catch (const MyException& e) {
    std::cout << "Type: " << typeid(e).name() << std::endl;
    throw;  // ПРАВИЛЬНО: Пробросит как MyException, сохранит тип
}

Объяснение: Когда вы используете throw e;, компилятор копирует исключение. Если переменная объявлена как базовый класс (const std::exception&), копия будет типа базового класса, и часть информации потеряется (slicing problem).

С throw; проблемы нет:

  • Пробрасывается оригинальное исключение, без копирования
  • Сохраняется точный тип исключения
  • Эффективнее (нет копирования)

Практические примеры

Пример 1: Логирование с пробросом

void process_file(const std::string& filename) {
    try {
        std::ifstream file(filename);
        if (!file) {
            throw std::runtime_error("Cannot open file");
        }
        // Обработка файла
    } catch (const std::exception& e) {
        // Логируем ошибку
        std::cerr << "Error in process_file: " << e.what() << std::endl;
        
        // Пробрасываем ДАЛЬШЕ для обработки на более высоком уровне
        throw;  // Выбросим то же исключение
    }
}

try {
    process_file("data.txt");
} catch (const std::runtime_error& e) {
    // На этом уровне можно обработать финально
    std::cout << "Final handler: " << e.what() << std::endl;
}

Пример 2: Частичная обработка с пробросом

void database_operation() {
    try {
        try {
            execute_query("SELECT * FROM users");
        } catch (const SQLError& e) {
            // Пытаемся переподключиться
            reconnect_to_database();
            throw;  // Если это не помогло, пробрасываем дальше
        }
    } catch (const SQLError& e) {
        // На этом уровне логируем в БД
        log_error(e);
        throw;  // Пробрасываем для последней обработки
    }
}

try {
    database_operation();
} catch (const SQLError& e) {
    // Финальная обработка
    notify_admin(e);
}

Пример 3: Условный пробрас

void handle_request(const Request& req) {
    try {
        process_request(req);
    } catch (const ValidationError& e) {
        // Этот тип обрабатываем локально
        send_response(400, e.what());
    } catch (const ServerError& e) {
        // Этот тип пробрасываем для глобальной обработки
        log_critical(e);
        throw;  // Отправим для обработки на более высоком уровне
    }
}

Исключение с дополнительной информацией

Когда нужно выбросить новое исключение с доп. информацией:

// Оборачиваем исключение в более специфичное
try {
    std::ifstream file(filename);
    file.read(buffer, size);
} catch (const std::ios_base::failure& e) {
    // Выбрасываем более специфичное исключение
    throw FileReadException("Failed to read: " + filename);
    // throw e; была бы неправильна, потеряем оригинальный контекст
}

// Лучше: Оборачиваем и пробрасываем оригинальное
try {
    std::ifstream file(filename);
    file.read(buffer, size);
} catch (const std::ios_base::failure& e) {
    // Логируем доп. информацию
    std::cerr << "While reading: " << filename << std::endl;
    // Пробрасываем оригинальное
    throw;
}

RAII паттерн с throw

class Transaction {
    Database& db;
    bool completed = false;
    
public:
    Transaction(Database& d) : db(d) {
        db.begin();
    }
    
    void do_work() {
        // Если выброшено исключение, деструктор откатит транзакцию
        execute_update("INSERT INTO table VALUES ...");
        execute_update("UPDATE table SET ...");
    }
    
    void commit() {
        try {
            db.commit();
            completed = true;
        } catch (...) {
            // Логируем, откатываем
            db.rollback();
            // Пробрасываем дальше
            throw;
        }
    }
    
    ~Transaction() {
        if (!completed) {
            try {
                db.rollback();
            } catch (...) {
                // Не выбрасываем из деструктора!
                std::cerr << "Rollback failed" << std::endl;
            }
        }
    }
};

Множественные уровни обработки

// Level 3 — выбрасывает исключение
void level3() {
    throw std::runtime_error("Deep error");
}

// Level 2 — логирует и пробрасывает
void level2() {
    try {
        level3();
    } catch (const std::exception& e) {
        std::cerr << "Level 2 error: " << e.what() << std::endl;
        throw;  // Пробросим дальше
    }
}

// Level 1 — логирует и пробрасывает
void level1() {
    try {
        level2();
    } catch (const std::exception& e) {
        std::cerr << "Level 1 error: " << e.what() << std::endl;
        throw;  // Пробросим в main
    }
}

int main() {
    try {
        level1();
    } catch (const std::exception& e) {
        // Финальная обработка
        std::cout << "Caught at main: " << e.what() << std::endl;
    }
}

// Вывод консоли:
// Level 2 error: Deep error
// Level 1 error: Deep error
// Caught at main: Deep error

Best Practices

1. Всегда используй throw; вместо throw e;

// Правильно
catch (const std::exception& e) {
    log(e.what());
    throw;  // ✓
}

// Неправильно
catch (const std::exception& e) {
    log(e.what());
    throw e;  // ✗
}

2. throw; только внутри catch блока

// ПРАВИЛЬНО
try { } catch (...) { throw; }

// НЕПРАВИЛЬНО
if (error) throw;  // Ошибка компиляции!

3. Не выбрасывай из деструктора

~MyClass() noexcept {
    try {
        cleanup();
    } catch (...) {
        // Обработать, но не выбросить
    }
}

Резюме

throw; — это способ:

  1. Пробросить текущее исключение дальше без модификации
  2. Сохранить точный тип исключения (нет слайсинга)
  3. Улучшить производительность (нет копирования)
  4. Логировать на разных уровнях иерархии вызовов
  5. Разделить ответственность за обработку ошибок

Это стандартный паттерн для систем с многоуровневой обработкой ошибок.