Что вы думаете о создании исключения для не найденного в C ++? - PullRequest
12 голосов
/ 27 сентября 2008

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

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

Давайте подумаем, что вы не хотите, чтобы в общедоступном интерфейсе класса были замечены опасные указатели. Как вы возвращаете в этом случае значение not found, выдавая исключение?

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

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

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

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

Ответы [ 11 ]

16 голосов
/ 27 сентября 2008

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

iterator find( const key_type& key );

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

6 голосов
/ 28 сентября 2008

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

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

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

5 голосов
/ 27 сентября 2008

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

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

5 голосов
/ 27 сентября 2008

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

Наилучшая практика в C ++ - один из двух следующих способов. Оба привыкли в STL:

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

/ EDIT:

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

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

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

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

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

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

2 голосов
/ 27 сентября 2008

STL Iterators?

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

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

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

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

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

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

: - р

Указатели

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

неопределенное значение

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

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

Исключения

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

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

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

и

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

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

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

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

И что?

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

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

; -)

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

Но если вы считаете, что клиент не будет распространять это «нулевое» значение или что они не должны распространять нулевой указатель (или умный указатель) в своем коде, то используйте ссылку, защищенную исключением. Добавьте метод 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() ;
   }
}

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

: -)

1 голос
/ 28 сентября 2008

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

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

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

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

1 голос
/ 28 сентября 2008

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();
}
1 голос
/ 27 сентября 2008

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

0 голосов
/ 28 сентября 2008

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

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

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

Вот в чём проблема, теперь пользователям нужно объяснять приведение в делах. Подобные указателю - прокси (такие как итераторы, ref-countted-ptrs и необработанные указатели) имеют краткий синтаксис 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», operator * и operator->.

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

0 голосов
/ 27 сентября 2008

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

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