понедельник, 30 августа 2010 г.

[prog] Случай, где была бы уместна логика на исключениях

У С++ников есть две страшные страшилки – использование goto и реализация логики на исключениях.

Ну, про goto все понятно – goto considered harmful и все такое ;) И действительно, в C++, в отличии от С (в котором goto часто используется для переход в блок очистки ресурсов в конце функции) есть достаточно средств без проблем обойтись без goto. Более того, в присутствии исключений именно такими средствами, а не goto и нужно пользоваться. Поэтому goto, действительно, harmful и о goto речи больше не будет.

А вот вторая страшилка действительно пострашнее и посерьезнее. Это использование исключений для логики программы.

Под катом много букв для тех, кто заинтересовался.

Если объяснять на пальцах, то логика на исключениях приводит вот к такому коду:

void process_data( const some_data_t & data )
  {
    try
      {
        do_some_processing( data );
      }
    catch( const everything_allright_t & )
      {
        finish_successful_processing();
      }
    catch( const something_bad_t & x )
      {
        finish_failed_processing( x );
      }
    catch( const shit_happened_t & x )
      {
        try_get_face_back_from_shit( x );
      }
  }

там, где можно было бы обойтись чем-то вроде:

enum data_processing_result_t
  {
    SUCCESSFUL,
    FAILED,
    SHIT_HAPPENED
  };

void process_data( const some_data_t & data )
  {
    const data_processing_result_t result = do_some_processing( data );
    switch( result )
      {
        case SUCCESSFUL:
          finish_successful_processing(); break;
        case FAILED:
          finish_failed_processing(); break;
        case SHIT_HAPPENED:
          try_get_face_back_from_shit(); break;
        default:
          throw_some_exception();
      }
  }

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

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

Однако, из любого правила есть исключения и исключения здесь не исключение ;) Для начала расскажу про ситуацию, с которой я столкнулся в последние несколько дней.

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

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

void
process_package(
  const source_package_t & src,
  good_package_list_t & good_receiver,
  bad_package_list_t & bad_receiver )
  {
    package_ptr_t pkg = make_package_from_source( src );

    failure_description_auto_ptr_t result;

    result = do_first_transformation( *pkg );
    if( result.get() )
      {
        // Пакет признан негодным.
        bad_receiver.add_failed_package( src, *result );
        return;
      }

    result = do_second_transformation( *pkg );
    if( result.get() )
      {
        bad_receiver.add_failed_package( src, *result );
        return;
      }
    ... // Еще несколько точно таких же действий.

    // Поскольку добрались сюда, значит пакет годный для дальнейшей
    // обработки.
    good_receiver.add( pkg );
  }

Что мне не нравится в этом, так это забор из однотипных if-ов. Для написания такой функции с ходу напрашивается замечательный прием copy-and-paste со всеми последующими прелестями. Чего, понятное дело, хотелось бы избежать.

В моем случае было просто – все функции трансформации пакетов имели одинаковый формат, поэтому я обошелся вектором указателей на них и циклом:

void
process_package(
  const source_package_t & src,
  good_package_list_t & good_receiver,
  bad_package_list_t & bad_receiver )
  {
    package_ptr_t pkg = make_package_from_source( src );

    typedef failure_description_auto_ptr_t (*pfn_transformator_t)(package_t &);
    pfn_transformator_t transformators[] =
      {
        &do_first_transformation,
        &do_second_transformation,
        &do_third_transformation,
        ...
      };
    for( std::size_t i = 0,
        i_max = sizeof(transformators)/sizeof(transformators[0]);
        i != i_max;
        ++i )
      {
        failure_description_auto_ptr_t result = (*transformators[i])( *pkg );
        if( result.get() )
          {
            // Пакет признан негодным.
            bad_receiver.add_failed_package( src, *result );
            return;
          }
      }

    // Поскольку добрались сюда, значит пакет годный для дальнейшей
    // обработки.
    good_receiver.add( pkg );
  }

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

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

void
process_package(
  const source_package_t & src,
  good_package_list_t & good_receiver,
  bad_package_list_t & bad_receiver )
  {
    try
      {
        package_ptr_t pkg = make_package_from_source( src );

        do_first_transformation( *pkg );
        do_second_transformation( *pkg );
        do_third_transformation( *pkg );
        ...

        // Поскольку добрались сюда, значит пакет годный для дальнейшей
        // обработки.
        good_receiver.add( pkg );
      }
    catch( const transformation_failed_ex_t & x )
      {
        // Пакет признан негодным.
        bad_receiver.add_failed_package( src, x.failure_description() );
      }
  }

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

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

В языке с паттерн-матчингом здесь не было бы проблемы. Скажем, на Scala (насколько я помню ее синтаксис) это можно было бы написать так:

abstract class PkgAnalysingResult
case class SuccessfulResult extends PkgAnalysingResult
case class IllegalFieldsResult(field: String, length: Int) extends PkgAnalysingResult
case class NonsupportedEncodingResult(encodingName: String) extends PkgAnalysingResult
case class UnknownDestinationResult(destName: Destination) extends PkgAnalysingResult
case class DeniedDestinationResult(
    destName: Destination,
    reason: String,
    errorCode: Int) extends PkgAnalysingResult

def analyzePkg(pkg: Package) { ... }

def processPkg(pkg: Package) {
  analyzePkg(pkg) match {
    case SuccessfulResult => ...
    case IllegalFieldsResult => ...
    case NonsupportedEncodingResult => ...
    case UnknownDestinationResult => ...
    case DeniedDestinationResult => ...
  }
  ...
}

Но в C++ так не напишешь (если только не задействовать какой-то из многочисленных VARIANT-ов). Зато в C++ можно задействовать исключения:

class analyzing_result_t : public std::exception { ... };
class illegal_fields_result_t : public analyzing_result_t { ... };
class nonsupported_encoding_result_t : public analyzing_result_t { ... };
class unknown_destination_result_t : public analyzing_result_t { ... };
class denied_destination_result_t : public analyzing_result_t { ... };

// Порождает исключение в случае каких-то проблем.
void analyze_pkg( const package_t & pkg ) { ... }

void process_pkg( const package_t & pkg )
  {
    try
      {
        analyze_pkg( pkg );
        ... // Дальнейшая нормальная обработка
      }
    catch( const illegal_fields_result_t & x ) { ... }
    catch( const nonsupported_encoding_result_t & x ) { ... }
    catch( const unknown_destination_result_t & x ) { ... }
    catch( const denied_destination_result_t & x ) { ... }
    catch( const analyzing_result_t & x )
      {
        ... // Какие-то действия на случай, если забыли сюда
            // добавить обработку еще одного варианта.
      }
  }

Конечно, с паттерн-матчингом не сравнить, но хоть какой-то хлеб.

Так что, логика на исключениях – это не есть хорошо и правильно. Но в C++ ее временами приходится использовать. Почти так же, как в чистом C goto.

37 комментариев:

Андрей Валяев комментирует...

Интересные темы ты затрагиваешь. Опасные даже. :)

do_first_transformation( *pkg );
do_second_transformation( *pkg );
do_third_transformation( *pkg );

Это все трансформации одного пакета?

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

Но в любом случае исключение everything_allright_t выглядит крайне странно.

Можно было бы попробовать разложить логику обработки пакетов на более конкретные шаги - чтобы было заранее известно по какой причине Каждый шаг ломается. тогда будет достаточно bool на выходе.

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

C другой стороны слишком длинный (больше одного) список условий сбоя говорит о том - что мы слишком глубоко залезли в суть процесса. :)

Исключения - действительно используются для исключительных ситуаций, но ведь таковым и является shit_happen для first_transform. :) Проблема в том что на высоком уровне мы подходим к проблемам не слишком обобщенно.

Вот эти вот разные функции в блоках catch мне не нравятся. :) Если бы они не были бы столь разными - мы могли бы ограничиться одним catch и были бы счастливы.

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

Множественные значения можно возвращать например через pair или tuple. :)

PS: Много буков, хоть пост ответный пиши. :)

Андрей Валяев комментирует...
Этот комментарий был удален автором.
eao197 комментирует...

>Интересные темы ты затрагиваешь. Опасные даже. :)

Ну дык! Людям жеж читать будет неинтересно, если я буду обсасывать широко известные банальности :)))

>Это все трансформации одного пакета?

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

>Стоимость исключений - она зависит от компилятора.

В данном случае этим можно пренебречь, поскольку потом все идет в логи и в БД.

>Но в любом случае исключение everything_allright_t выглядит крайне странно.

А это уже следствие передергивания, чтобы лучше было видно, к чему можно придти.

eao197 комментирует...

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


Это был один из вариантов. Но мне он не нравится из-за того, что функции-трансформаторы становятся завязанными на более общую инфораструктуру. Так, они должны уже знать про bad_receiver. Что, имхо, не есть хорошо.

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

>Функции трансформации, если предположить что у них разные прототипы, можно унифицировать как нибудь. функторы всякие например. :)

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

Только все это по сложности и запутанности не будет уступать логике на исключениях.

>PS: Много буков, хоть пост ответный пиши. :)

Да я не против, с удовольствием почитаю.

Quaker комментирует...

Почти так же, как в чистом C goto.
А как это будет выглядеть в чистом C с goto, можете привести набросок кода?

Анонимный комментирует...

Думаю, это тебе будет полезно. Есть в виде DJVU.

http://www.ozon.ru/context/detail/id/5011068/

eao197 комментирует...

>>Почти так же, как в чистом C goto.
>А как это будет выглядеть в чистом C с goto, можете привести набросок кода?


Похоже, что здесь я неоднозначно высказался. Подразумевалось вот что.

В чистом С есть общепринятая идиома по очистке ресурсов с помошью goto:

int f()
{
FILE * from = NULL;
FILE * to = NULL;
int result = -1;

from = fopen(...);
if( !from ) goto clean;

to = fopen(...);
if( !to ) goto clean;
...
clean:
if(from) fclose(from);
if(to) fclose(to);

return result;
}

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

Я не имел в виду, что обработку пакетов на чистом C следовало бы делать через goto.

eao197 комментирует...

@san: у нас в офисе есть какая-то книга по рефакторингу, по-моему, именно эта. Я как-то ее пролистывал, но желания прочесть не возникло. Не помню почему.

имя комментирует...

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

package_ptr_t pkg = make_package_from_source( src );

failure_description_auto_ptr_t result = SUCCESS;

result && ( result = do_first_transformation( *pkg ) );
result && ( result = do_second_transformation( *pkg, a, 7 ) );
result && ( result = do_third_transformation( *pkg, b, c, d, 142 ) );

if( result )
{
// Пакет годный для дальнейшей обработки.
good_receiver.add( pkg );
}
else
{
// Пакет признан негодным.
bad_receiver.add_failed_package( src, result );
}

Проблема этого подхода только в том, что result не должен быть указателем, а должен быть объектом типа, у которого определен оператор приведения к bool. Указатель нафиг все обрушит (хотя может и можно иметь перегруженный оператор *).

Да, юзая расширения жцц это можно еще подсократить (за счет макроса, допускающего вложенность). Ниче не знаю про расширения МС-овского компилятора.

Еще: АлгТД похоже вполне возможно достаточно многословно выразить на с++, если будет контрпример -- интересно.

eao197 комментирует...

@имя:

>result && ( result = do_first_transformation( *pkg ) );
result && ( result = do_second_transformation( *pkg, a, 7 ) );
result && ( result = do_third_transformation( *pkg, b, c, d, 142 ) );


Прикольно. Спасибо, я до этого способа не додумался.

>Еще: АлгТД похоже вполне возможно достаточно многословно выразить на с++

Наверняка можно. В конце-концов, C++ можно рассматривать всего лишь как высокоуровневый ассемблер.

А для данной задачи (анализ результатов трансформация сообщения) в C++ можно было бы применить Visitor, наверное. Получилось бы как в паттерн-матчинге: с контролем от компилятора за всеми возможными вариантами.

Андрей Валяев комментирует...

Чистый код - читал...

Не могу сказать что очень уж полезная книга. Все идеи неоднократно звучали в других изданиях.

Эта книга - сборник историй как бы. Помимо Роберта Мартина к ней приложили руки и другие специалисты.



Более целенаправленные издания всетаки получше будут. Рефакторинг в частности, хотя живых примеров в Рефакторинге не хватает, а в Чистом коде есть. :)


result && ( result = do_first_transformation( *pkg ) );

Помоему выглядит немного странно, Понятно, что это оптимизированная форма
if (result ...) {
result = ...
}

if (result ...) {
result = ...
}

return result...

Может быть этот паттерн как нибудь называется?

Можно кстати сделать функцию - которая будет обрабатывать все исключения и принимать в качестве простого или шаблонного параметра трансформатор или функтор от трансформатора :)

Кстати логика всеравно не ясна до конца. Если один трансформатор не срабатывает - это не фатально ли для данного сообщения?

eao197 комментирует...

>Кстати логика всеравно не ясна до конца. Если один трансформатор не срабатывает - это не фатально ли для данного сообщения?

Фатально. Обработку нужно прерывать при обнаружении первой проблемы с сообщением.

Андрей Валяев комментирует...

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

Главное чтобы перехватываемые исключения выглядели логично по отношению к текущему коду.

Кроме того код стал бы проще, если бы не пытался формировать два списка.

Как используется bad_receiver?

Что конкретно делает эта функция? process_data - совершенно неконкретное название. :)

eao197 комментирует...

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

Я, наверное, потерял нить рассуждений. Исходный посыл был такой: если есть цепочка вызовов, которую нужно прерывать после первого неудавшегося вызова, то удобно заставить функции бросать исключения и обернуть всю цепочку try/catch-ем. Однако, если же причины неудачи нужно еще и индивидуально проанализировать, то тогда try/catch становится аналогом паттерн-матчинга.

Андрей Валяев комментирует...

Сорри :)

Собственно необходимость анализировать отдельно каждый вызов - и является проблемой. :)

А зачем это надо делать индивидуально? Почему нельзя это делать централизованно?

Собственно критерий отбора всего один - хороший пакет или плохой. (вероятность 50% :D)

Поэтому разветвление алгоритма тоже должно быть одно.

Какие еще индивидуальные критерии пакетов надо анализировать?

имя комментирует...

@Андрей Валяев

> Помоему выглядит немного странно,

Да, на с++ так не пишут (это можно простить, но трудно простить ту проблему с указателями, вот что плохо). Так пишут на перле.

@Евгений Охотников

а на руби так не пишут?

> Наверняка можно. В конце-концов, C++ можно рассматривать всего лишь как высокоуровневый ассемблер.

не-не-не

речь идет о том, чтобы компилятор с++ проверял то же самое, что проверяет компилятор языка с АлгТД,

ну и с точки зрения программера оно смотрелось бы аналогично АлгТД

> то тогда try/catch становится аналогом паттерн-матчинга

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

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

Кстати: как MSVC отнесется к:

void f() throw() { throw 1; }

Емнип ни comeau, ни g++ варнингов не давали

> можно было бы применить Visitor, наверное.

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

( а еще есть вариант

if( some_error* e = dynamic_cast< some_error* >(result) ) { ... }
else if( other_error* e = dynamic_cast< other_error* >(result) ) { ... }

по писанине и (не)надежности примерно так же, как исключения, но предположу, что побыстрее, т.к. стэк не раскручивается )

eao197 комментирует...

@Андрей Валяев:

>Собственно необходимость анализировать отдельно каждый вызов - и является проблемой. :)

Ага, так и есть.

>А зачем это надо делать индивидуально? Почему нельзя это делать централизованно?

А что значит "централизовано"?

>Какие еще индивидуальные критерии пакетов надо анализировать?

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

eao197 комментирует...

@имя:

>а на руби так не пишут?

Не припомню такого. На Ruby я бы писал так:

result = do_first_translation(msg)
result = do_second_translation(msg) unless result
result = do_third_translation(msg) unless result

>речь идет о том, чтобы компилятор с++ проверял то же самое, что проверяет компилятор языка с АлгТД,

Я не Александреску ;) У меня нет идей о том, как это проделать.

>С практической точки зрения это мне кажется самым лучшим из возможного на с++.

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

eao197 комментирует...

@имя:

>Кстати: как MSVC отнесется к:

void f() throw() { throw 1; }


На код:

void f() throw()
{
throw std::runtime_error( "oops!" );
}

MSVC2008 выдал предупреждение:

t1.cpp(7) : warning C4297: 'f' : function assumed not to throw an exception but does
__declspec(nothrow) or throw() was specified on the function

Однако, если преобразовать код:

void g()
{
throw std::runtime_error( "oops!" );
}

void f() throw()
{
g();
}

то никаких предупреждений.

Андрей Валяев комментирует...

> А что значит "централизовано"?

Централизованно - это значит один кетч на все трансформаторы. :)

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

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

try {
trandform1(msg);
} catch (const bad_message &bm) {
bm.postop();
throw;
}

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

eao197 комментирует...

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

Да, можно было бы. По сути, в простой ситуации как раз и следовало бы так поступить. Но:

1. Это уже будет логика на исключениях. Неправильные данные в пакете -- это ожидаемо и нормально, а мы бросаем исключение. Те сторонники "чистоты идеалов", которые говорят, что логика на исключениях -- это плохо, будут сильно недовольны :)

2. Все-таки не очень правильно, когда устанавливается тесная связь между диагностированием ошибки и ее обработкой. Допустим, мы введем исключение illegal_field_length_ex_t, в котором метод postop выполняет какую-то специфическую функциональность. Это значит, что на эту функциональнось будет завязана и функции-трансформаторы.

Поэтому более подходящим я считаю способ с визитором:

class problem_processor_t;
class transformation_problem_ex_t : public std::runtime_error {
public: virtual void handle(problem_processor_t &) const = 0;
};
class illegal_field_length_ex_t : public transformation_problem_ex_t {
public: virtual void handle(problem_processor_t & p) const { p.process(*this); }
};
...
try {
trandform1(msg);
} catch (const transformation_problem_ex_t & x) {
concrete_problem_processor_t processor;
x.handle( processor );
}

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

eao197 комментирует...

По-моему, как раз ув.тов.имя и подразумевал такое решение, когда мы говорили об Visitor-е.

Андрей Валяев комментирует...

> Это уже будет логика на исключениях. Неправильные данные в пакете -- это ожидаемо и нормально..

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

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

> Все-таки не очень правильно, когда устанавливается тесная связь между диагностированием ошибки и ее обработкой.

Да, я тут подумал, что метод обработки адреса не в состоянии знать что process_message должен предпринять по этому поводу.

Но с другой стороны трансформатор вполне может знать. :)

> Допустим, мы введем исключение illegal_field_length_ex_t

Если вводить специфические исключения по ситуациям - то мы возвращаемся к ...

catch(a) ..
catch(b) ..
catch(c) ..
catch(d) ..

В то время как process_message не должен вдаваться в такие детали. :)
Его дело отделить зерна от плевел. :) То есть либо true либо false. :)

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

eao197 комментирует...

>Это ожидаемо и нормально на уровне обработки пакетов - функция process_message не исключает того, что сообщение плохое.

Ну вот ты и заговорил как человек, который не считает, что логика на исключениях -- это плохо! Попался! :)))

>Если вводить специфические исключения по ситуациям - то мы возвращаемся к ...

Нет не возвращаемся. Все классы a, b, c, d и пр., производны от общей базы. В process_message ловится только база, а потом пойманное исключение передается на обработку конкретному Visitor-у, который уже знает про a, b, c и d. И бонус здесь в том, что если появляется какой-то новый класс g, то код с его использованием не скомпилируется вовсе.

Андрей Валяев комментирует...

> Ну вот ты и заговорил как человек, который не считает, что логика на исключениях -- это плохо

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

Это конечно логика. Логика обработки исключительных ситуаций. :)

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

Опасаясь логики на исключениях можно дойти до того - что вообще их не обрабатывать, а то логика блин. :D

Если реакция на ошибки всегда одинакова - то опять таки один набор catch все покроет. Меня больше пугает ситуация, когда в каждом случае необходимо реагировать особым образом на особые ошибки. Всмысле на каждый транслятор... 8-( )

eao197 комментирует...

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

Ну, по-моему, для исключений есть достаточно четкий критерий: если мы сталкиваемся с тем, чего быть не должно, то это преобразуется в исключение. Например, открыли файл, попытались в него записать, а он почему-то закрыт. Это исключительная ситуация. А вот если мы в функцию unlink передаем пустое имя, то это не исключительная ситуация, а вполне ожидаемая -- неверный аргумент.

>Меня больше пугает ситуация, когда в каждом случае необходимо реагировать особым образом на особые ошибки. Всмысле на каждый транслятор... 8-( )

ИМХО, вариант с Visitor-ом как раз это покрывает.

Андрей Валяев комментирует...

Мы, к примеру, хотим выделить из адреса домен - а домена нету. Это исключительная ситуация или нормальное состояние для функции выделения домена? :)

Пока все трансляторы у тебя в цикле и блок реакций один - visitor - нормально.

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

Логика на исключениях - Это использование исключений в тех ситуациях где их быть не должно. :)

try { cmp (a, b); }
catch (a_great) { ... }
catch (b_great) { ... }
catch (equal) { ... }

:)

К исключительности ситуаций можно относиться по разному. Нулевое значение - в каких-то случаях вполне допустимо, а в каких-то случаях исключительная ситуация (деление на ноль, нулевой указатель).. Все относительно.

eao197 комментирует...

>К исключительности ситуаций можно относиться по разному. Нулевое значение - в каких-то случаях вполне допустимо, а в каких-то случаях исключительная ситуация (деление на ноль, нулевой указатель)

It depends...

Например, если ты пишешь y = sqrt(x) + sqrt(z), а x отрицательный, то здесь уместно исключение. Поскольку вернуть код ошибки из sqrt нельзя. И проблема в том, что ты написал свои вычисления неправильно, раз в x попало отрицательное значение.

С другой стороны, если ты пишешь калькулятор, где x вводится пользователем. Поскольку пользователя ты не контролируешь, ты можешь либо перед вычислениями проверять введенные пользователем значения, а потом смело вычислять y, либо же ты напишешь:

try {
y = sqrt(x) + sqrt(z);
} catch( some ) { ... }

В этом случае у тебя уже появляется логика на исключениях. Я так считаю. Возможно, я подхожу к вопросу излишне догматично.

>Все относительно.

Именно. Поэтому к страшилкам о том, что логика на исключениях -- это плохо, лично я отношусь скептически.

Андрей Валяев комментирует...

Но требовать от sqrt - чтобы она корректно обрабатывала отрицательные значения, откуда бы они не поступили - глупо. Для этой функции отрицательное значение это совершенно исключительная ситуация.

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

Конечно это логика. Логика обработки исключительных ситуаций. :)

Избегать ее - это вообще не отлавливать исключения в программе - нехай падает. :) А то логика на исключениях проглотит нам мозг. :)

Андрей Валяев комментирует...

Просто нужно стараться различать исключительность и типичность...

try {
...
} catch (Database::EmptySet &) {
/* ok */
}

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

eao197 комментирует...

>Но требовать от sqrt - чтобы она корректно обрабатывала отрицательные значения, откуда бы они не поступили - глупо. Для этой функции отрицательное значение это совершенно исключительная ситуация.

Это зависит от формата sqrt. Если у нее формат double sqrt(double), тогда требовать ничего нельзя. Функция изначально расчитана на корректные данные.

Но если изменить формат на что-то вроде:

abstract class SqrtResult
case class Value(v: double) extends SqrtResult
case class DomainError(x: ErrorDescription) extends SqrtResult

def sqrt(x: double): SqrtResult = ...

то уже не исключительная ситуация.

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

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

Андрей Валяев комментирует...

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

Еще можно было бы сделать чтобы с DomainError можно было бы различные операции производить - с тем же результатом.

Помню были раньше программируемые калькуляторы. MK-61 вроде и другие. Там были Error'ы, причем над ними можно было проводить различные операции, что порою приводило к интересным последствиям, слово Error трансформировалось всячески. :) В играх использовали. Страшный Згогг третьего порядка. :)

eao197 комментирует...

>Но лично я не боюсь исключений, просто логика должна быть.

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

Kodt комментирует...

В С++ есть ADT - это boost::variant, например. И даже boost::any

eao197 комментирует...

@Kodt: я специально оговорил случай об неиспользовании различных версий variant-ов.

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

Andrey Valyaev комментирует...

boost - это практически стандарт. :)

Эдак можно и STL не рассматривать. ибо это не сам язык, а библиотека на n мегабайт. :)

Вообще в наши дни мегабайты - дешевы.

eao197 комментирует...

>boost - это практически стандарт. :)

Простите мне мой французкий, но нифига это не стандарт. Такой же велосипед, как и ACE, Poco или STLsoft.

>Эдак можно и STL не рассматривать. ибо это не сам язык, а библиотека на n мегабайт. :)

Как раз STL -- это часть языка. Он в стандарте расписан. И если появляется необходимость, например, портироваться на платформу, где у компилятора STL-я нет, то смело можно просить совсем другие деньги.

>Вообще в наши дни мегабайты - дешевы.

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

Да и возвращаясь к ADT и pattern-matching. Можно попробовать сравнить объем кода на Boost.Variant и Scala для обработки одних их тех же вещей, с контролем компилятором за полнотой. Тогда сразу станет видно, где библиотека, а где родная поддержка языка.