Использование throw для замены return в C++ не пустых функциях

В функциях C++ хорошая практика - заменить return на throw? Например, у меня есть следующий код

// return indices of two numbers whose sum is equal to target
vector<int> twoSum(vector<int>& nums, int target) {
    for(int i=0; i<nums.size()-1; ++i)
        for(int j=i+1; j<nums.size(); ++j)
        {
            if(nums[i] + nums[j] == target) return vector<int>{i, j};
        }
    // return vector<int>{};
    throw "no solution";
}

Код выше компилируется с моим GCC 7.2.

Ответы

Ответ 1

Концепции этого ответа взяты из языка программирования C++ Бьярна Страуструпа.

КОРОТКИЙ ОТВЕТ

Да, исключение может быть использовано как метод возврата значения. Примером является следующее для функции поиска двоичного дерева:

void fnd(Tree∗ p, const string& s)
{
    if (s == p−>str) throw p; // found s
    if (p−>left) fnd(p−>left,s);
    if (p−>right) fnd(p−>right,s);
}


Tree∗ find(Tree∗ p, const string& s)
{
    try {
       fnd(p,s);
    }
    catch (Tree∗ q) {
        // q->str==s
        return q;
    }
    return 0;
}

Однако этого следует избегать, потому что:

  • они позволяют вам отделить код ошибки от "обычного кода", делая вашу программу намного более читабельной, понятной и управляемой. Если вы используете их в качестве метода возврата, это больше не выполняется.
  • возможны неэффективности, поскольку реализации исключений основаны на предположении, что они используются в качестве методов обработки ошибок.

Кроме того, существуют дополнительные ограничения:

  • исключения должны быть копируемого типа
  • исключения могут обрабатывать только синхронные события
  • их следует избегать в критичной ко времени системе
  • их следует избегать в больших старых программах, в которых управление ресурсами представляет собой случайный беспорядок (бесплатное хранилище бессистемно управляется с использованием обнаженных указателей, новостей и удалений), а не полагается на какую-то систематическую схему, такую как дескрипторы ресурсов (векторы строк).

Более длинный ответ

Исключением является объект, выданный для представления возникновения ошибки. Это может быть любой тип, который может быть скопирован, но настоятельно рекомендуется использовать только определенные пользователем типы, специально определенные для этой цели. Исключения позволяют программисту явно отделять код обработки ошибок от "обычного кода", делая программу более читабельной.

Прежде всего, исключения предназначены для управления синхронными событиями, а не асинхронными. Это одно из первых ограничений.

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

Это имеет некоторое очарование, но его следует избегать, потому что это может вызвать путаницу и неэффективность. Страуструп предлагает:

Когда это вообще возможно, придерживайтесь представления "обработка исключений - обработка ошибок". Когда это сделано, код делится на две категории: обычный код и код обработки ошибок. Это делает код более понятным. Кроме того, реализации механизмов исключения оптимизируются на основе предположения, что эта простая модель лежит в основе использования исключения.

Таким образом, в основном следует использовать исключения для возврата значения, потому что

  • реализация исключений оптимизирована, если предположить, что они используются для обработки ошибок, а не для возврата значений, следовательно, они могут быть неэффективными для этого;
  • Они позволяют отделить код ошибки от обычного кода, делая код гораздо более читабельным и понятным. Все, что помогает сохранить четкую модель того, что является ошибкой и как она обрабатывается, должно цениться.

Существуют программы, которые по практическим или историческим причинам не могут использовать исключения (ни как обработка ошибок, ни тем более):

  • Критически важный для времени компонент встроенной системы, в котором работа должна быть гарантированно завершена в указанное максимальное время. В отсутствие инструментов, которые могут точно оценить максимальное время распространения исключения от throw до catch должны использоваться альтернативные методы обработки ошибок.
  • Большая старая программа, в которой управление ресурсами представляет собой случайный беспорядок (бесплатное хранилище бессистемно управляется с использованием обнаженных указателей, news и delete), а не полагается на какую-то систематическую схему, такую как дескрипторы ресурсов (string vector).

В вышеупомянутых случаях предпочтительны традиционные методы предварительного исключения.

Ответ 2

В функциях C++ хорошая практика - заменить return на throw?

Возврат - это не то, что можно заменить броском вообще.

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

Является ли это "хорошей практикой", а какой случай "исключительным", субъективны. Например, для такой функции поиска, как ваша, неудивительно, что не может быть решения, и я бы сказал, что выбрасывание не подходит.

Часто есть другие альтернативы метанию. Сравните ваш алгоритм с чем-то вроде std::string::find который возвращает индекс начала подстроки. В случае, когда подстрока не существует, она возвращает "не значение" std::string::npos. Вы можете сделать то же самое и решить, что индекс -1 возвращается, когда результат не найден. Существует также универсальный способ добавления не значащего представления к типу в случаях, когда ни одно из существующих представлений не может быть зарезервировано для этой цели: std::optional.

PS Вектор, вероятно, не является хорошим выбором для возврата пары чисел. std::pair может быть лучше, или пользовательский класс, если у вас есть хорошие имена для чисел.

Ответ 3

return и throw имеют две разные цели и не должны рассматриваться как взаимозаменяемые. Используйте return когда у вас есть действительный результат, чтобы отправить его обратно вызывающей стороне. С другой стороны, используйте throw когда происходит какое-то исключительное поведение. Вы можете получить представление о том, как другие программисты используют throw, используя функции из стандартной библиотеки и отмечая, когда они генерируют исключения.

Ответ 4

В дополнение к другим ответам, здесь также есть производительность: перехват исключения приводит к накладным расходам времени выполнения (см. Этот ответ) по сравнению с предложением if.

(Конечно, мы говорим о микросекундах... То, имеет ли это отношение, зависит от вашего конкретного случая использования.)

Ответ 5

Функция должна генерировать исключение, когда она не может выполнить свое постусловие. (Некоторые функции могут также генерировать исключения, когда их предварительные условия не выполняются; это другая тема, в которую я не буду здесь вдаваться.) Поэтому, если ваша функция должна возвращать пару целых чисел из вектора, суммирующего для данной цели, тогда она не имеет другого выбора, кроме как бросить исключение, когда оно не может его найти. Но если функциональный контракт позволяет ему возвращать значение, указывающее, что он не смог найти такую пару, то он не должен выдавать исключение, поскольку у него есть возможность выполнить контракт.

В твоем случае:

  • Вы можете заставить вашу функцию возвращать пустой вектор, когда она не может найти два целых числа, суммирующих с заданной целью. Тогда ему никогда не нужно выбрасывать исключение.
  • Или вы можете заставить вашу функцию возвращать std::optional<std::pair<int, int>>. Ему никогда не нужно выбрасывать исключение, потому что он может просто вернуть пустое необязательное, когда не может найти подходящую пару.
  • Однако, если вы сделаете так, чтобы он возвращал std::pair<int, int>, он должен выдать исключение, потому что нет разумного значения, которое нужно вернуть, когда он не может найти подходящую пару.

Как правило, программисты C++ предпочитают писать функции, которым не нужно генерировать исключения, чтобы сообщать о простых "разочарованиях", таких как сбои поиска, которые легко предвидеть и обрабатывать локально. Преимущества возврата значений вместо выдачи исключений широко обсуждаются в других местах, поэтому я не буду перефразировать это обсуждение здесь.

Таким образом, объявление "сбой" путем выдачи исключения обычно ограничивается следующими случаями:

  • Функция является конструктором, и она просто не может установить свой инвариант класса. Он должен генерировать исключение, чтобы вызывающий код не видел сломанный объект.
  • Возникает "исключительное" состояние, которое должно обрабатываться на более высоком уровне, чем у вызывающего. Звонящий, скорее всего, не знает, как оправиться от состояния. В таких случаях использование механизма исключения освобождает вызывающего абонента от необходимости выяснять, как действовать, когда возникает исключительное условие.

Ответ 6

То, что вы упомянули, не является хорошей практикой программирования. Замена оператора return на throw недопустима в коде производственного уровня, особенно на платформах автоматизированного тестирования, которые генерируют списки всех исключений в качестве способа подтверждения или опровержения определенной функциональности. При разработке кода вы должны учитывать потребности тестировщиков программного обеспечения. throw просто не взаимозаменяем с return. throw используется, чтобы сигнализировать, что программная ошибка произошла из-за некоторых неожиданных явлений. return используется для оповещения о завершении метода. Обычно для return кодов ошибок используется return, но возвращаемые значения не приводят к прерыванию программы так же, как throw. Кроме того, у throw есть возможность завершить программу, если она обрабатывается неправильно.

По сути, эффективная практика - использовать throw когда в методе обнаружена значительная ошибка, но всегда должен быть чистый возврат, если такая ошибка не обнаружена. Вы можете сделать эту замену? Может быть, и, возможно, это будет работать логически... но это не хорошая практика и, конечно, не популярная реализация.

Ответ 7

Нет, throw не является хорошей семантической заменой для return. Вы хотите использовать throw только тогда, когда ваш код сделал что-то, чего не следует делать, чтобы не обозначать совершенно допустимый отрицательный результат функции.

Как правило, исключения означают, что во время выполнения вашего кода произошло что-то ненормальное или неожиданное. Глядя на цель вашей функции, появление двух целых чисел в переданном векторном суммировании в target является очень возможным результатом функции, поэтому результат не является ненормальным и поэтому не должен рассматриваться как исключительный.

Ответ 8

Ваш вопрос не зависит от языка.

Возврат и Бросок имеют разные цели.

  • Возврат предназначен для возврата значения; в то время,
  • Бросок для того, чтобы бросить исключение; а также,
    • Исключение следует создавать в действительно исключительных условиях.

Бонусный контент (от Стив Макконнеллс, код 2)

Вот все доступные вам альтернативы, когда вы сталкиваетесь с ситуацией, когда вы не можете нормально вернуться:

  1. Вернуть нейтральное значение как -1 из метода, который должен возвращать счет чего-либо;
  2. Вернуть ближайшее цифровое значение, например, 0, на цифровой спидометр автомобиля, когда он движется задним ходом;
  3. Возвратите то же значение, что и предыдущий вызов, как показание температуры на термометре, когда вы читаете каждую секунду, и вы знаете, что показания не сильно отличаются за такой короткий интервал;
  4. Вернуть следующий допустимый фрагмент данных, например, когда вы читаете строки из таблицы, а определенная строка повреждена;
  5. Установить/вернуть статус ошибки/код/​​объект, который часто встречается в вызовах библиотеки C++;
  6. Выведите сообщение в окне предупреждения, но будьте осторожны, чтобы не выдавать слишком много информации, которая может помочь злоумышленнику;
  7. Войдите в файл;
  8. Создайте исключение, как вы думаете;
  9. Вызовите центральный обработчик ошибок/подпрограмму, если так устроена ваша система;
  10. Shutdown.

Далее вам не нужно выбирать только один из вышеперечисленных вариантов. Вы можете сделать комбинацию, например, войти в файл и, отображая сообщение для пользователя.

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

Ответ 9

Тот факт, что функция вызывает последний вызов, не означает, что она заменяет return. Это просто поток логики.

Вопрос не должен быть:

это хорошая практика, чтобы заменить возвращение на бросок?

Вместо этого следует рассказать о: как определить свой API и контракты.

Если вы хотите гарантировать пользователям функции, что vector никогда не будет пустым, то выбрасывание исключения - хорошая идея.

Если вы хотите гарантировать, что ваша функция не выбрасывает исключения, а при определенных условиях возвращает пустой vector, то выбрасывание - плохая идея.

В обоих случаях пользователь должен знать, какие действия он должен предпринять, чтобы правильно использовать вашу функцию.

Ответ 10

Я думаю, что это неправильный вопрос, спрашивающий, можно ли заменить return на throw. return и throw делают две совершенно разные вещи:

  • return используется для возврата обычных результатов функций.
  • throw - это предпочтительный способ реагирования на ошибки, когда значение результата не существует.

Альтернативной реакцией на ошибки может быть возвращение специальных значений кода ошибки. Это имеет некоторые недостатки, например:

  • Использование функции становится более сложным, если вы всегда должны помнить коды ошибок и т.д.
  • Вызывающий может забыть обработать ошибку. Когда вы забудете обработчик исключений, вы получите необработанное исключение. Весьма вероятно, что вы обнаружите эту ошибку.
  • Ошибка может быть слишком сложной, чтобы правильно выразить ее одним кодом ошибки. Исключением может быть объект, содержащий всю необходимую информацию.
  • Ошибки обрабатываются в четко определенной позиции (обработчик исключений). Это сделает ваш код намного понятнее, чем проверка возвращаемого значения для специальных значений после каждого вызова.

В вашем примере ответ зависит от семантики функции:

Если пустой вектор является приемлемым возвращаемым значением, вам следует использовать return, если не найдено ни одной пары.

Если ожидается, что всегда ДОЛЖНЫ быть совпадающие пары, то обнаружение пары не является очевидной ошибкой. Вы должны использовать throw в этом случае.