Подтвердить что ты не робот

Функция с пользовательским типом возврата и условиями возврата "ложных"?

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

Моя функция выглядит примерно так:

Cell CSV::Find(std::string segment) {
  Cell result;
  // Search code here.
  return result;
}

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

Я думал о добавлении логического метода внутри Cell, чтобы проверить, что когда-либо Cell.data пусто или нет (Cell.IsEmpty()). Но я думаю, что эта проблема слишком сложна?

4b9b3361

Ответ 1

Существует три общих подхода:

  • Использовать исключения. Это то, что ответят в ответ на вопрос Вирсавии.
  • Вернуть std::optional<Cell> (или какой-либо другой тип, который может содержать или не содержать фактический Cell).
  • Верните bool и добавьте параметр Cell &.

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

Если часть конструкции этой функции состоит в том, что ее можно использовать, чтобы определить, действительно ли сегмент действителен, исключения не подходят, и мой предпочтительный выбор был бы std::optional<Cell>. Это может быть недоступно в вашей стандартной реализации библиотеки (это функция С++ 17); если нет, boost::optional<Cell> может быть полезным (как упоминается в ответе Ричарда Ходжеса).

В комментариях вместо std::optional<Cell> пользователь вы предложили expected<Cell, error> (не стандартный С++, но предлагаемый для будущего стандарта, и реализуется вне пространства имен std до тех пор). Это может быть хорошим вариантом добавить некоторые указания о том, почему не удалось найти Cell для параметра segment, если есть несколько возможных причин.

Третий вариант, который я включил в основном для полноты. Я это не рекомендую. Это популярный и в целом хороший образец на других языках.

Ответ 2

Является ли эта функция запросом, который действительно не мог найти ячейку, или это императив, где ожидается, что ячейка будет найдена?

Если первое, верните необязательный (или указатель на нуль) ячейку.

Если последнее, выбросьте исключение, если оно не найдено.

Бывший:

boost::optional<Cell> CSV::Find(std::string segment) {
  boost::optional<Cell> result;
  // Search code here.
  return result;
}

последние: как у вас есть.

И, конечно же, существует альтернативный вариант С++ 17:

#include <variant>
#include <string>

struct CellNotFound {};
struct Cell {};

using CellFindResult = std::variant<CellNotFound, Cell>;


CellFindResult Find(std::string segment) {
  CellFindResult result { CellNotFound {} };

  // Search code here.
  return result;
}

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

void cellsAndStuff()
{
    std::visit(overloaded
    {
        [&](CellNotFound)
        {
            // the not-found code
        },
        [&](Cell c)
        {
            // code on cell found
        }
    }, Find("foo"));
}

Ответ 3

Способ С++ для устранения сбоев - это определить класс исключения формы:

struct CSVException : std::exception{};

В вашей функции вы затем throw один из тех, кто находится в ветке отказа:

Cell CSV::Find(std::string segment) {
  Cell result;
  // Search code here.
  if (fail) throw CSVException();
  return result;
}

Затем вы обрабатываете случай сбоя с блоком try catch на вызывающем сайте.

Если, однако, ветка "fail" - это нормальное поведение (субъективно, но только вы можете быть судьей нормальности), то действительно на самом деле наполнить какой-то индикатор отказа внутри Cell или, возможно, даже изменить тип возврата на std::optional<Cell>.

Ответ 4

Если вы можете использовать С++ 17, другим подходом было бы использовать тип std::optional в качестве возвращаемого значения. Это оболочка, которая может содержать или не содержать значение. Затем вызывающий может проверить, действительно ли ваша функция вернула значение и обрабатывать случай, когда он этого не сделал.

std::optional<Cell> CSV::Find(std::string segment) {
  Cell result;
  // Search code here.
  return result;
}

void clientCode() {
   auto cell = CSV::Find("foo");
   if (cell)
      // do stuff when found
   else
      // handle not found
}

Ответ 5

Следующая опция использует несколько возвращаемых значений:

std::pair<Cell, bool> CSV::Find(std::string segment) {
  Cell result;
  // Search code here.
  return {result, found};
}

// ...

auto cell = CSV::Find("foo");
if (cell->second)
  // do stuff with cell->first

Булевский флаг указывает, найден ли запрошенный Cell или нет.

PROs
  • известный подход (например std::map::insert);
  • довольно прямой: значение и индикатор успеха - это возвращаемые значения функции.
ПРОТИВ
  • скрытность first и second, которая требует, чтобы всегда помнить относительные позиции значений внутри пар (С++ 17 структурированные привязки частично разрешить эту проблему);
  • возможная потеря безопасности (вызывающий код должен проверить, есть ли значение результата, перед его использованием).

Подробнее

Ответ 6

Для синтаксического анализа обычно лучше избегать std::string и вместо этого использовать std::string_view; если С++ 17 недоступен, минимально функциональные версии можно легко взломать.

Кроме того, важно также отслеживать не только то, что было проанализировано, но и остаток.

Есть две возможности отслеживать остаток:

  • принимает изменяемый аргумент (по ссылке),
  • возвращает остаток.

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


Затем вам нужно изучить возможные потенциальные ошибки и какие механизмы восстановления вы хотите. Это сообщит о дизайне.

Например, если вы хотите иметь возможность анализировать плохо сформированные CSV-документы, то разумно, чтобы Cell мог представлять плохо сформированные ячейки CSV, и в этом случае интерфейс довольно прост:

std::pair<Cell, std::string_view> consume_cell(std::string_view input) noexcept;

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

С другой стороны, если вы хотите поддерживать хорошо сформированные CSV-документы, тогда разумно сигнализировать об ошибках через исключения и что Cell сможет удерживать фактические ячейки:

std::pair<std::optional<Cell>, std::string_view> consume_cell(...);

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

Интерфейс С++ для итераторов является немного неуклюжим (так как вам нужен "конец", а конец неизвестен до разбора), однако я рекомендую придерживаться его, чтобы иметь возможность использовать итератор с циклами for. Однако, если вы хотите уйти от него, по крайней мере, сделать его легко работать с while, например std::optional<Cell> cell; while ((cell = row.next())) { ... }.