Здравствуйте, antropolog, Вы писали:
A>Здравствуйте, push, Вы писали:
P>>Я тебя понял — ты (КЕП!) пытаешься показать, что асинхронный механизм с синхронным плохо совмещается.
A>я пытаюсь показать что асинхронность с исключениями плохо совмещается
Это не проблема исключений, это проблема того, что ты пытаешься синхронный механизм применить к асинхронному.
A>Хорошо хоть ты признал что исключения здесь не работают. Жаль только что это заняло у тебя так много времени.
Какой-то детский сад. Сам придумал, сам опроверг.
P>>Она должна вернуть тебе std::future, который прозрачно бросит исключение при доступе к данным.
A>И в какой момент я должен доставать оттуда значение?
Ровно в тот момент, когда оно тебе понадобится.
P>>В третьих, если у тебя полностью всё асинхронное, то никаких on_result(FILE) быть не должно, так как обработка ошибок должна вестись в асинхронном режиме тоже. Должно быть on_success и on_fail, в коллбек параметра которого пихается код/объект проблемы.
A>кому должен? Или перефразируя, чем это лучше возврата значения в коллбек?
Здравому смыслу: избегаем макаронного кода, получаем простоту изменения логики заменой подходящим коллбеком нужной части и инкапсуляцию обработки ошибок в отдельной функции (так как обработка ошибок — это не игнорирование их). Но код ошибки, или объект обозначающий ошибку будет в on_fail присутствовать тоже. Далее, чтобы доставить его в нужное место для обработки — нужны те же слёзы, что и для
синхронного случая.
Но как уже отвечали, можно использовать исключения вместе с короутинами (я про это был не в курсе). И опять получается проблема решена. И опять с помощью исключений.
P>>Но это не код возврата, тут возврата нет, потому и проблем с ними нет. Тут проблемы другого рода.
A>Дефинируй тогда что такое "возврат". Возможно у тебя какая-то своя, особая терминология.
Ладно, показываю ещё раз в чём проблема кодов возврата и как исключения её решают.
Допустим у нас обработка ошибок в проекте ведётся через коды возврата. Пусть есть следующий стек вызовов:
... -> func1() -> ... -> func2() -> ... -> func3() -> func4() -> ...
-> func5() -> ...
-> func6() -> func7()
-> func8()
-> ...
Все функции имеют такую сигнатуру (чтобы пользоваться кодами возврата мы уже имеем ограничение на сигнатуру функции):
uint func(.....)
где uint содержит код возврата — он не имитация bool, может содержать разные значения (нет прав, отсутствует база, отсутствует связь и т.д.).
Каждая функция может выдавать свой набор кодов возвратов.
func1() -> самое удобное место, к примеру, запросить креденшелы у пользователя. В случае дальнейшей ошибки доступа мы должны вернуться сюда, где можем описать пользователю проблему и попросить креденшелы с повышенными привилегиями, если таковые у него есть.
func2() -> самое удобное место для выбора вида связи с сервером. Если что-то в дальнейшем пойдёт не так с коммуникациями — то тут мы можем попросить выбрать другой способ связи.
Допустим func5 кроме прочего может возвращать код "недостаточно прав", func7 + func8 + ... — кроме прочего могут возвращать код "пакет данных повреждён"
Итак, проверять коды возврата нужно будет во всех функциях вне зависимости от успеха или неудачи.
В очень удачном случае у нас возникнет проблема "недостаточно прав", код которой мы просто можем передавать выше до func1() и там обработать.
В обычном же случае (с правильной декомпозицией) у нас будут идти снизу(func7, func8, ...) коды возврата вида "пакет данных повреждён", которые мы не можем обработать в func2(), так как мы не знаем о чём речь: про сеть, про локальные файлы, про кеш и т.д. Нам надо по дороге преобразовать его где-то в "канал не доступен", чтобы возможно было обработать в func2() и выбрать другой канал.
И таких кодов возврата как правило далеко не один.
И это масштаб бедствий только для 8 функций, в масштабе приложения всё гораздо печальнее. Я уже не говорю, про человеческий фактор, когда кто-то не обработал исключение, или затёр неверным — дебажить это очень муторно.
В случае же исключений:
1) нет ограничений на сигнатуру функций
2) мы без гемора ловим там, где хотим и то, что хотим — хоть одно исключение, хоть подмножество
3) мы не боимся, что исключение случайно где-то перетрётся.
4) отсутствие мусорного кода (того, что требуется писать всё время для поддержки кодов возврата)
5) по пути исплючения мы можем без гемора собрать весь контекст проблемы
P>>В полностью асинхронной программе архитектура должна быть быть такой, чтобы все проблемы можно было обработать на месте возникновения, что сильно ограничивает область её применения, либо архитектуру (к примеру, полностью событийная архитектура, где ни коды возврата, ни исключения не используются).
A>А обосновать? В чём проблема с кодами возврата? Коллбеки в которые передаются возвращаемые значения используются повсеместно в асинхронном коде. В чём ограничение?
Проблема с кодом ошибки та же, что и в синхронном коде, только посущественнее. Посмотри схему вызовов выше и представь, что они асинхронные. Проблема в функции func8 "пакет данных повреждён" должна быть обработана в func2() как "канал не доступен". Чтобы это было возможным, код должен пройти через func3, где после нескольких попыток "пакет данных повреждён" будет преобразован в "канал не доступен" и направлен в func2(). Чтобы такой вакханалией управлять в масштабе всего асинхронного приложения — всё должно быть построено на каком-либо типе машин состояний. Это настолько усложняет управление всем этим, что коды ошибок обрабатываются либо сразу на месте, либо подбирается другая архитектура.
A>Ну и повторю вопрос. В какой момент мне доставать из future?
Да в какой хочешь, хоть сразу
P>>данная сигнатура не позволяет клиенту узнать почему нет данных — то ли файл не нашли, то ли он был пустой и могли искать дальше.
A>возможно потому что клиент об этом не должен знать?
Я даже не знаю, что тебе тут сказать. То есть клиенту не только пофиг есть ли данные локально, а и вообще есть ли они в наличии в принципе? Так зачем тогда все те приседания поиском файла (перебор значений — это и есть поиск) если они не влияют на клиент — сразу принимать, что нет данных и точка! И пляшем дальше. Тогда там у вас архитектурные проблемы. Архитектурой их и надо решать.
P>>А где собственно обработка кодов возврата? Её тут нет.
A>А суслик?
Там даже в псевдокоде нет обработки кодов возврата. Там просто игнор всех проблем. Это пригодно для личных мелких утилит, но не для реального проекта.
P>>Вот пример, что такое коды исключений: https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa365430(v=vs.85).aspx , а вот возможные значения https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms681381(v=vs.85).aspx .
P>>И их надо обрабатывать для каждой функции.
A>А 20 лет назад майкрософт учила всех использовать HRESULT. Своя голова на плечах есть?
И HRESULT эпично вошло в историю примером того как "удобно" было всем этим пользоваться. А потом MS подумало и решило решить все проблемы одним махом — появился .NET
А там обработка проблем реализована через....о боже....ты не поверишь....через исключения! (вот так поворот)
P>>так как чрезвычайно редко в реальных программах проблему возможно адекватно обработать на месте её возникновения.
A>именно в месте возникновения редко можно, а вот на один вызов выше- практически всегда.
В нормальных проектах (ни микроутилиты) — чем он больше тем дальше место обработки от возникновения. Более того, чем проект больше — тем больше мест, где обрабатывается одна и та же проблема (обработка -> бросание нового исключения и т.д.).
A>Т.е. ты всеръёз утверждаешь, что прежде чем загрузить данные, я должен сначала а) посмотреть в коде как они заружаются б) написать код проверки условий успешной загрузки до вызова loadFile ? Вот уже где фейспалм так фейспалм.
В случае кодов возврата — это как раз то, чем и придётся заниматься.
A>Ну т.е. boost::exception отпадает. Как отпадают вообще любые типы исключений, не обрабатываемые фремворком. Тогда какие ты исключения будешь бросать? Или ты заранее знаешь все фреймворки, в которых будет использоваться твой код? А если он будет использоваться в двух сразу? 
) Код пишется под конкретный фреймворк, в случае чего ты не сможешь заменить один фреймворк другим без обработки кода напильником. Как в случае исключений, так и в случае кодов возврата. Да и вообще в любом случае.
P>>Ты описываешь типичный антипаттерн программирования.
A>где об этом можно почитать?
https://sourcemaking.com/antipatterns/functional-decomposition
P>>Это всё уже было в истории. А потом изобрели ООП (в частности инкапсуляцию, которая как раз и скрывает от клиента то, что ему не нужно знать) и жизнь стала налаживаться.
A>Ну, то что ты предполагаешь нарушать инкапсуляцию заради исключений мы уже выяснили, но всё же спрошу, какими боком инкапсуляция соотносится с исключениями?
A>Чисто логически OpenFile понятия не имеет, проблема для меня то, что он не мог открыть файл или нет. Как следствие бросать эксепшн — дурость. Надо возвращать результат. А уж клиентский код решит, проблема для него этот результат или нормальная ситуация. Это настолько очевидные вещи что я недоумеваю, почему надо об этом говорить.
"Чисто логически OpenFile понятия не имеет, проблема для меня то, что он не мог открыть файл или нет" — в верном ключе мыслишь. Он не знает что для тебя проблема: наличие ли файла, наличие ли прав, возможность ли открытия (например файл заблочен другой программой) и т.д. Это знать ему нафиг не упрёлось — но он знает, что это для него самого проблема. Он не может выполнить то, что ты от него требуешь. А значит ты его используешь в ненадлежащем контексте. Он должен тебе отсигнализировать, что он сделать не может и по какой причине не может.
"Как следствие бросать эксепшн — дурость. Надо возвращать результат." — неверно, разобрано вверху в чём минуса кодов возврата.
"А уж клиентский код решит, проблема для него этот результат или нормальная ситуация." — и опять верно.
"Это настолько очевидные вещи что я недоумеваю, почему надо об этом говорить." — у меня тоже самое. Ход твоих мыслей вроде верен, но потом какой-то скачёк — и опять игнор проблемы.