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

Что вы думаете о том, чтобы исключить исключение из С++?

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

class list {
    public:
        value &get(type key);
};

Предположим, что вы не хотите, чтобы в общедоступном интерфейсе класса отображались опасные указатели, как вы возвращаете не найденное в этом случае исключение?

Каков ваш подход к этому? Вы возвращаете пустое значение и проверяете его пустое состояние? Я действительно использую подход throw, но я вводил метод проверки:

class list {
   public:
      bool exists(type key);
      value &get(type key);
};

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

Как вы это сделаете?

4b9b3361

Ответ 1

STL справляется с этой ситуацией, используя итераторы. Например, класс std:: map имеет аналогичную функцию:

iterator find( const key_type& key );

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

Ответ 2

Правильный ответ (по словам Александреску):

Optional и Enforce

Прежде всего, используйте Accessor, но безопаснее, не изобретая колесо:

boost::optional<X> get_X_if_possible();

Затем создайте помощник enforce:

template <class T, class E>
T& enforce(boost::optional<T>& opt, E e = std::runtime_error("enforce failed"))
{
    if(!opt)
    {
        throw e;
    }

    return *opt;
}

// and an overload for T const &

Таким образом, в зависимости от того, что может означать отсутствие значения, вы либо явно проверяете:

if(boost::optional<X> maybe_x = get_X_if_possible())
{
    X& x = *maybe_x;

    // use x
}
else
{
    oops("Hey, we got no x again!");
}

или неявно:

X& x = enforce(get_X_if_possible());

// use x

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

Ответ 3

Не используйте исключение в таком случае. С++ имеет нетривиальные служебные издержки для таких исключений, даже если исключение не выбрасывается, и оно значительно упрощает рассуждение о коде (см. Исключение безопасности).

Лучшая практика в С++ - один из двух способов. Оба используются в STL:

  • Как отметил Мартин, верните итератор. На самом деле, ваш итератор может быть typedef для простого указателя, и ничего не говорит против него; на самом деле, поскольку это согласуется с STL, вы можете даже утверждать, что этот способ превосходит возврат ссылки.
  • Верните a std::pair<bool, yourvalue>. Это делает невозможным изменение значения, однако, поскольку вызывается copycon из pair, который не работает с членами референдума.

/EDIT:

Этот ответ породил довольно противоречие, видимое из комментариев и не очень заметное из многих полученных вниз. Я нашел это довольно неожиданным.

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

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

Собственно, я упомянул два аспекта: безопасность работы и исключений. Я считаю, что последний был довольно бесспорным. Хотя крайне сложно дать надежные исключения (самый сильный, конечно, "некратный" ), я считаю, что это важно: любой код, который гарантированно не генерирует исключений, облегчает рассуждение всей программы. Многие эксперты С++ подчеркивают это (например, Скотт Мейерс в пункте 29 "Эффективный С++" ).

О скорости. Мартин Йорк отметил, что это больше не применяется в современных компиляторах. Я почтительно не согласен. Язык С++ делает необходимым, чтобы среда отслеживала во время выполнения пути кода, которые могут быть размотаны в случае исключения. Теперь это накладные расходы на самом деле не такие большие (и это довольно легко проверить). "нетривиальный" в моем вышеприведенном тексте, возможно, был слишком сильным.

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

Ответ 4

Проблема с exists() заключается в том, что вы в конечном итоге дважды будете искать вещи, которые существуют (сначала проверьте, есть ли там, затем найдите его снова). Это неэффективно, особенно если (как показывает его название "список" ), ваш контейнер является тем, где поиск - O (n).

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

Ответ 5

Итераторы STL?

Идея "итератора", предложенная передо мной, интересна, но реальной точкой итераторов является навигация через контейнер. Не как простой аксессор.

Если вы принадлежите одному из многих, то итераторы - это путь, потому что вы сможете использовать их для перемещения в контейнере. Но если ваш аксессор - это простой getter, способный вернуть либо значение, либо факт, что существует no value, то ваш итератор, возможно, является только прославленным указателем...

Это приводит нас к...

Умные указатели?

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

Вы должны выбрать: либо ваше значение уже находится внутри общего указателя, а затем вы можете вернуть этот общий указатель (или слабый указатель). Или Ваше значение находится внутри необработанного указателя. Затем вы можете вернуть указатель строки. Вы не хотите возвращать общий указатель, если ваш ressource еще не находится внутри общего указателя: Мир забавных вещей случится, когда ваш общий указатель выйдет из сферы действия, удалите свое значение, не сообщив вы...

: - р

Указатели?

Если в вашем интерфейсе четко указано его право собственности на его ресурсы, а также на то, что возвращаемое значение может быть NULL, тогда вы можете вернуть простой, необработанный указатель. Если пользователь вашего кода достаточно глупый, игнорируйте контракт на интерфейс вашего объекта или играйте в арифметику или что-то еще с вашим указателем, то он/она будет достаточно глупым, чтобы нарушить любой другой способ, по которому вы захотите вернуть значение, поэтому не беспокойтесь с умственно отсталыми...

Undefined Значение

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

Но не добавляйте значение "undefined" в свой класс Value , потому что проблемы, которую вы задали: вы в конечном итоге поднимаете войну "ссылки против указателя" на другой уровень маразм. Пользователи кода хотят, чтобы объекты, которые вы им дали, были либо Ок, либо не существовали. Чтобы проверить каждую другую строку кода, этот объект по-прежнему действителен, это боль, и по вашей ошибке будет бесполезно скомпоновать код пользователя.

Исключения

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

Например, STL std::vector имеет два доступа к его значению через индекс:

T & std::vector::operator[]( /* index */ )

и

T & std::vector::at( /* index */ )

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

С другой стороны, at - это throwing. Это означает, что если вы выходите за пределы диапазона вектора, вы получите чистое исключение. Этот метод лучше, если вы хотите делегировать другому коду обработку ошибки.

Я использую personnaly [], когда я получаю доступ к значениям внутри цикла или что-то подобное. Я использую at, когда я чувствую, что исключение - это хороший способ вернуть текущий код (или код вызова), что-то пошло не так.

Итак, что?

В вашем случае вы должны выбрать:

Если вам действительно нужен молниеносный доступ, тогда проблема с метательным доступом может быть проблемой. Но это означает, что вы уже использовали профилировщик своего кода, чтобы определить, что это узкое место, не так ли?

; -)

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

Но если вы считаете, что клиент не будет распространять это "нулевое" значение или что они не должны распространять указатель NULL (или интеллектуальный указатель) в своем коде, тогда используйте ссылку, защищенную исключением. Добавьте метод hasValue, возвращающий логическое значение, и добавьте бросок, если пользователь попытается получить значение, даже если его нет.

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

// If you want your user to have this kind of code, then choose either
// pointer or smart pointer solution
void doSomething(MyClass & p_oMyClass)
{
   MyValue * pValue = p_oMyClass.getValue() ;

   if(pValue != NULL)
   {
      // Etc.
   }
}

MyValue * doSomethingElseAndReturnValue(MyClass & p_oMyClass)
{
   MyValue * pValue = p_oMyClass.getValue() ;

   if(pValue != NULL)
   {
      // Etc.
   }

   return pValue ;
}

// ==========================================================

// If you want your user to have this kind of code, then choose the
// throwing reference solution
void doSomething(MyClass & p_oMyClass)
{
   if(p_oMyClass.hasValue())
   {
      MyValue & oValue = p_oMyClass.getValue() ;
   }
}

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

: -)

Ответ 6

Как насчет возврата shared_ptr в качестве результата. Это может быть null, если элемент не найден. Он работает как указатель, но он позаботится об освобождении объекта для вас.

Ответ 7

Accessor?

Идея "итератора", предложенная передо мной, интересна, но реальной точкой итераторов является навигация через контейнер. Не как простой аксессор.

Я согласен с paercebal, итератором является итерация. Мне не нравится, как это делает STL. Но идея аксессора кажется более привлекательной. Итак, что нам нужно? Контейнер, подобный классу, который похож на логическое для тестирования, но ведет себя как исходный тип возвращаемого значения. Это было бы возможно с операторами-литыми.

template <T> class Accessor {
    public:
        Accessor(): _value(NULL) 
        {}

        Accessor(T &value): _value(&value)
        {}

        operator T &() const
        {
            if (!_value)
               throw Exception("that is a problem and you made a mistake somewhere.");
            else
               return *_value;
        }

        operator bool () const
        {
            return _value != NULL;
        }

    private:
        T *_value;
};

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

Accessor <type> value = list.get(key);

if (value) {
   type &v = value;

   v.doSomething();
}

Ответ 8

(Я понимаю, что это не всегда правильный ответ, и мой тон немного силен, но вы должны рассматривать этот вопрос, прежде чем решать другие более сложные альтернативы):

Итак, что случилось с возвратом указателя?

Я видел это много раз в SQL, где люди будут делать все возможное, чтобы никогда не иметь дело с столбцами NULL, например, у них есть какая-то инфекционная кончина или что-то в этом роде. Вместо этого они ловко придумывают "пустое" или "не-там" искусственное значение, такое как -1, 9999 или даже что-то вроде "@X-EMPTY-X @".

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

Ответ 9

то, что я предпочитаю делать в таких ситуациях, это бросать "get", и для тех обстоятельств, когда значение производительности или неудача являются общими, есть функция "tryGet" по строкам "bool tryGet" (тип ключа, значение ** pp ) "whoose contract заключается в том, что если true возвращается, то * pp == действительный указатель на какой-либо объект else * pp имеет значение null.

Ответ 10

@aradtke, вы сказали.

Я согласен с paercebal, итератором является итерации. Мне не нравится способ STL делает. Но идея доступа кажется более привлекательным. Итак, что нам нужно? Контейнер, подобный классу, который похож на логическое для тестирования, но ведет себя как оригинальный тип возврата. Это было возможно с операциями литья. [..] Теперь, любая предсказуемая проблема?

Во-первых, ВЫ НЕ ХОТИТЕ ОПЕРАТОР bool. Для получения дополнительной информации см. Safe Bool idiom. Но о вашем вопросе...

Здесь проблема, пользователи должны теперь выявлять приведение в случаях. Pointer-like- proxies (такие как итераторы, ref-counted-ptrs и raw указатели) имеют краткий синтаксис get. Предоставление оператора преобразования не очень полезно, если вызывающим абонентам приходится ссылаться на него с дополнительным кодом.

Начиная с вашего подтверждения, например, наиболее краткого способа его написания:

// 'reference' style, check before use
if (Accessor<type> value = list.get(key)) {
   type &v = value;
   v.doSomething();
}
// or
if (Accessor<type> value = list.get(key)) {
   static_cast<type&>(value).doSomething();
}

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

// 'reference' style, skip check 
type &v = list.get(key);
v.doSomething();
// or
static_cast<type&>(list.get(key)).doSomething();

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

// 'pointer' style, check before use
if (Accessor<type> value = list.get(key)) {
   value->doSomething();
}

// 'pointer' style, skip check 
list.get(key)->doSomething();

Оба довольно хороши, но синтаксис указателя/итератора немного короче. Вы можете дать стиль 'reference' функции-члена 'get()'... но это уже тот оператор *() и operator → () для.

В стиле "указатель" Accessor теперь есть оператор "неуказанный bool", оператор * и оператор- > .

И угадайте, какой... raw указатель отвечает этим требованиям, поэтому для прототипирования list.get() возвращает T * вместо Accessor. Затем, когда дизайн списка стабилен, вы можете вернуться и написать Accessor, указательный тип Proxy.

Ответ 11

Интересный вопрос. Это проблема в С++ для использования исключительно ссылок, которые, я думаю, в Java ссылки более гибкие и могут быть нулевыми. Я не могу вспомнить, если это законный С++ для принудительной нулевой ссылки:

MyType *pObj = nullptr;
return *pObj

Но я считаю это опасным. Снова в Java я бы выбрал исключение, поскольку это распространено там, но я редко вижу исключения, которые так свободно используются в С++. Если бы я делал puclic API для многократного использования компонента С++ и должен был возвращать ссылку, я предполагаю, что я бы пошел маршрут исключения. Мое реальное предпочтение заключается в том, чтобы API возвращал указатель; Я считаю, что указатели являются неотъемлемой частью С++.