Здравствуйте, Николай Ивченков, Вы писали:
НИ>Юрий Жмеренецкий:
НИ>>>У меня есть подозрения, что тут всё же подразумеваются некие "слабые" концепции, выражающие общие требования к классу без предусловий выполнения этих требований. Т.е. класс удовлетворяет требованиям "слабой" концепции, если существует специфичный для него набор предусловий, при выполнении которых он поддерживает все операции в соответствии с указанной семантикой.
ЮЖ>>Для каких целей нужны такие концепции?
НИ>Для тех же, что и обычные концепции.
Для обычной концепции мне достаточно знать только ее контракт, для слабых — то же самое, плюс контракт 'реализатора'. Чем-то мне это напоминает dynamic_cast...
ЮЖ>>
НИ>Данная функция, помимо принадлежности arg к Lockable, должна описывать характер использования arg (впрочем, тогда в ряде случаев можно вообще обойтись без упоминания о Lockable ). Исходя из этого описания пользователь функции должен сделать выводы о том, соблюдаются предусловия для его случая или нет.
Т.е. фактически предусловия пользователь должен выводить сам. В чуть более сложных случаях — вложенные вызовы, использование в f других таких концепций — это приведет к тому, что ему будет необходимо отследить весь путь выполнения (исходя из его случая). Пока я в этому вижу только минусы...
НИ>Фактически "слабая" концепция — это просто незавершённая концепция. Автор класса, формулируя конкретные предусловия, создаёт из неё завершённую концепцию (но без явного имени).
Чем-то напоминает CRTP. Я имел ввиду более 'худший' вариант: 'динамическую' концепцию, которая определена не статически для типа, а для значений, например после выполнения delete для указателя, последний перестает удовлетворять Assignable requirements. + не замкнутые относительно себя концепции.
ЮЖ>>что ты скажешь по поводу следующих двух вопросов?
НИ>1) Соответствует ли бустовой концепции Lockable класс std::mutex из N2914 — 30.4.1.1? НИ>2) Может ли этот std::mutex быть использован для инстанцирования boost::unique_lock?
Если отвечать сразу — то нет в обоих случаях, хотя бы из-за типов исключений =) Если не принимать это во внимание — то, на первый взгляд, можно ответить да на оба вопроса. Но для точных ответов нужен более тщательный анализ. + некторые формулировки стандарта выглядят достаточно спорно, например
30.4.1/12 (mutext::lock)
Error conditions:
...
— resource_deadlock_would_occur — if the current thread already owns the mutex and is able
to detect it.
30.4.3.2.2/4 (unique_lock::lock)
Error conditions:
...
— resource_deadlock_would_occur — if the current thread already owns the mutex (i.e., on entry,
owns is true).
Пследнее — это ни что иное как завуалированное предусловие, более того — формально, 30.4.3.2.2/2/3 делают невозможным возникновение ситуации c resource_deadlock_would_occur. Хотя я не исключаю что я что-то упустил.
Re[14]: api design: return code or exception - formal criter
Здравствуйте, Andrew S, Вы писали:
AS>Тем не менее. Представим, что реализация делает именно то, что и должна. Изначальный вопрос звучал так — почему тут исключение, а не assert? И то, и то может быть использовано как сигнализатор нарушения контракта.
Нарушение контракта всегда является дефектом, независимо от того с чьей стороны произошло нарушение (клиент/реализация). Возникновение исключения (вообще, не рассматривая unique_lock)- не является дефектом — это невозможность выполнения постусловий при выполненных предусловиях. Состояние объекта, как таковое, существует только в том случае если, его можно наблюдать (является observable). И вот как раз постусловия формулируются в виде предикатов, которые оперируют эти видимым состоянием. Ключевое свойство постусловий — их можно проверить (в принципе, но для этого нет необходимости) и на их основании клиент может сделать определенные выводы. Собственно сам по себе вызов функции необходим только для перевода объекта в состояние, в котором постусловия являются выполненными.
AS>В общем, предлагаю закругляться, никаких новых аргументов уже не появляется.
Ок. обсуждение unique_lock можно считать закрытым. Но у меня еще пара вопросов — считаете ли Вы, что например функция std::string::erase(iterator, iterator) должна проверять итераторы на валидность (и кидать исключение, в случае если дальнейшее выполнение прведет к порче 'состояния')?
Изменится или нет ответ на этот вопрос в таких случаях: 1) существует предусловие говорящее о том что передаваемые итераторы должны быть валидны (в как это формулируется для объекта string) 2) такое предусловие отсутствует.
Если в обоих случаях ответ — да, то Вы приверженец т.н. Defensive Programming Style, я же в свою очередь, нахожусь по другую сторону баррикад — второй случай дла меня неприемлим вообще (почти всегда), для первого — ответ нет. И, разумеется, этот выбор основан не на пустом месте.
Re[3]: api design: return code or exception - formal criteri
Здравствуйте, crable, Вы писали:
V>>К чему это всё скажите вы? А к тому что иногда поддержка исключений библиотекой приводит к образованию некоторого пользовательского болота, организованного недобросовестными программистами использующими библиотеку, из которого вам впоследствии будет трудно выбраться. V>>Иногда лучше вообще не пользоваться исключениями и "заставлять" пользователя обрабатывать код возврата. C>А мне показалось, что лучше не ставить ассерты перед throw и недобросовестеыми тут выглядят как раз программисты библиотеки, сделавшие это.
Тогда придётся перезапускать приложение с включёнными галками ловить исключения, что тоже не есть хорошо.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Re[3]: api design: return code or exception - formal criteri
Здравствуйте, Erop, Вы писали:
V>>Иногда лучше вообще не пользоваться исключениями и "заставлять" пользователя обрабатывать код возврата. E>IMHO, пользователей библиотеки, которые "развели болото" не удастся заставить верно обрабатывать коды возврата...
Для начало можно сделать коды возврата обязательными параметрами всех нужных функций.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Re[15]: api design: return code or exception - formal criter
AS>>В общем, предлагаю закругляться, никаких новых аргументов уже не появляется.
ЮЖ>Ок. обсуждение unique_lock можно считать закрытым. Но у меня еще пара вопросов — считаете ли Вы, что например функция std::string::erase(iterator, iterator) должна проверять итераторы на валидность (и кидать исключение, в случае если дальнейшее выполнение приведет к порче 'состояния')?
В debug time — да. В релизе — нет. Причины
1. Производительность.
2. Нарушение контракта не обеспечит нарушение контракта подчиненного объекта.
В случае unique_lock нарушение контракта ведет к нарушению контракта подчиненного объекта.
ЮЖ>Изменится или нет ответ на этот вопрос в таких случаях: 1) существует предусловие говорящее о том что передаваемые итераторы должны быть валидны (в как это формулируется для объекта string) 2) такое предусловие отсутствует.
Юрий Жмеренецкий:
ЮЖ>Для обычной концепции мне достаточно знать только ее контракт, для слабых — то же самое, плюс контракт 'реализатора'. Чем-то мне это напоминает dynamic_cast...
ЮЖ>>>
НИ>>Данная функция, помимо принадлежности arg к Lockable, должна описывать характер использования arg (впрочем, тогда в ряде случаев можно вообще обойтись без упоминания о Lockable ). Исходя из этого описания пользователь функции должен сделать выводы о том, соблюдаются предусловия для его случая или нет.
ЮЖ>Т.е. фактически предусловия пользователь должен выводить сам. В чуть более сложных случаях — вложенные вызовы, использование в f других таких концепций — это приведет к тому, что ему будет необходимо отследить весь путь выполнения (исходя из его случая). Пока я в этому вижу только минусы...
В N2914 вводятся "Mutex requirements", по сути являющиеся "слабой" концепцией. Эта концепция подразделяется на две: "non-recursive mutex" и "recursive mutex". std::unique_lock инстанцируется типом, соответствующим "слабой" концепции Mutex. Различие в использовании std::unique_lock, инстанцированного non-recursive mutex-ом, и std::unique_lock, инстанцированного recursive mutex-ом, проводится уже в описании его функций-членов:
explicit unique_lock(mutex_type& m);
Precondition: If mutex_type is not a recursive mutex the calling thread does not own the mutex.
По поводу повторного вызова lock у объекта типа std::unique_lock<T> мне пока неясно — то ли там undefined behavior, то ли генерация исключения.
НИ>>1) Соответствует ли бустовой концепции Lockable класс std::mutex из N2914 — 30.4.1.1? НИ>>2) Может ли этот std::mutex быть использован для инстанцирования boost::unique_lock?
ЮЖ>Если отвечать сразу — то нет в обоих случаях, хотя бы из-за типов исключений =)
Ну, в принципе, можно рассмотреть некий MutexWrapper над std::mutex, где стандартные исключения будут транслироваться в бустовские, а всё остальное будет, как в std::mutex.
ЮЖ>Если не принимать это во внимание — то, на первый взгляд, можно ответить да на оба вопроса.
Ну, как же? У std::mutex::lock есть предусловие: эту функцию можно вызывать только если текущий поток не владеет данным мьютексом. Концепция Lockable (в Boost) такого предусловия не содержит.
Re[12]: api design: return code or exception - formal criter
Здравствуйте, Николай Ивченков, Вы писали:
НИ>В N2914 вводятся "Mutex requirements", по сути являющиеся "слабой" концепцией. Эта концепция подразделяется на две: "non-recursive mutex" и "recursive mutex".
Это две разных концепции. Упоминание "If mutex_type is not a recursive mutex" только подтверждает это. Фактически просто switch между двумя концепциями, а не использование одной.
ЮЖ>>Если не принимать это во внимание — то, на первый взгляд, можно ответить да на оба вопроса.
НИ>Ну, как же? У std::mutex::lock есть предусловие: эту функцию можно вызывать только если текущий поток не владеет данным мьютексом.
Где оно сформулировано? пока я вижу только "if a function does not specify any further preconditions, there will be no “Requires” paragraph."
Кроме того есть 17.6.3.11/1, и если ты говоришь что предусловие все же имеется, то должно ли оно удовлетворять 'стандартному' определению?
Плюс, аналога 17.6.3.11/1 для буста нет. Т.е. здесь возможна ситуация двух различных определений предусловий. Да и мое определение предусловий расходится с определением стадарта (они там, кстати, нарушили свой любимый zero-overhead principle).
Re[4]: api design: return code or exception - formal criteri
Здравствуйте, Vain, Вы писали:
V>Для начало можно сделать коды возврата обязательными параметрами всех нужных функций.
А что это даст, кроме дальнейшего снижения удобства использования библиотеки?
IMHO, качественный клинтский код -- это забота его авторов. Библиотека должна позволять писать качественный, и не должна мешать это делать или провоцировать на некачественный. Но бороться?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[13]: api design: return code or exception - formal criter
Юрий Жмеренецкий:
НИ>>В N2914 вводятся "Mutex requirements", по сути являющиеся "слабой" концепцией. Эта концепция подразделяется на две: "non-recursive mutex" и "recursive mutex".
ЮЖ>Это две разных концепции. Упоминание "If mutex_type is not a recursive mutex" только подтверждает это. Фактически просто switch между двумя концепциями, а не использование одной.
Если говорить о типе, которым инстанцируется std::unique_lock, то тут используется именно одна "слабая" концепция Mutex. А то, что такому описанию std::unique_lock можно сопоставить некое эквивалентное по смыслу описание, выраженное через две концепции с более конкретизированными предусловиями — это уже другой вопрос.
ЮЖ>>>Если не принимать это во внимание — то, на первый взгляд, можно ответить да на оба вопроса.
НИ>>Ну, как же? У std::mutex::lock есть предусловие: эту функцию можно вызывать только если текущий поток не владеет данным мьютексом.
ЮЖ>Где оно сформулировано?
30.4.1.1/3:
The behavior of a program is undefined if:
[...]
— a thread that owns a mutex object calls lock() or try_lock() on that object
С точки зрения пользователя std::mutex нет никакой разницы между этим undefined behavior и undefined behavior, возникающим в результате несоблюдения условий, которые были бы явным образом специфицированы как предусловия. Т.е. фактически отсутствие владения мьютексом со стороны текущего потока можно считать предусловием для вызова lock.
ЮЖ>Плюс, аналога 17.6.3.11/1 для буста нет.
Если исходить из того же принципа (появление undefined behavior следует считать следствием нарушения предусловий), и если попытку повторного захвата boost::mutex считать приводящей к undefined behavior (у меня в тестовой программе попытка повторного захвата boost::mutex приводит к зависанию потока), то можно прийти к тем же выводам и относительно boost::mutex.
ЮЖ>они там, кстати, нарушили свой любимый zero-overhead principle.
Где именно?
Re[4]: api design: return code or exception - formal criteri
Здравствуйте, Andrew S, Вы писали:
AS>На мой взгляд в этом случае именно что ассерты. Пользователь продолжает пользовать невалидный объект — значит ошибка в логике программы, а ошибку в логике надо по возможности отлавливать никак не в релизе. Эксепшены только в случае, если слой более низкого уровня вернул "плохую" ошибку. На плохой хендл, например, многие api так и сделают, так что автоматически без "лишней" рантайм проверки в релизе все будет выглядеть как надо.
Вы не путаете отладку программы с обработкой ошибок?
коды возврата и исключения это инструменты для обработки ошибок, assert макрос для отладки. Конечно их можно использовать не по назначению, но имеет ли смысл ?
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Ищу работу, 3D, SLAM, computer graphics/vision.
Re[2]: api design: return code or exception - formal criteri
Здравствуйте, Vain, Вы писали:
V> После появились программисты библиотеки, которые поняли, что каждый раз включать галку в студии останавливаться на исключении в отладчике не катит и вставили ассерт перед самим throw.
Объясните подробнее пожалуйста этот кусок.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Ищу работу, 3D, SLAM, computer graphics/vision.
Re[6]: api design: return code or exception - formal criteri
Здравствуйте, Andrew S, Вы писали:
AS> Assert ровно такое же средство уведомления о нарушении контракта, как и исключение.
То же что и в предыдущем посте.
Исключение это средство ссобщить об ошибке рограммы, Assert об ошибке програмиста.
например в псевдокоде.
std::copy(It begin, It end, it2 begin2);
Существует задокументированный контракт что begin и end обязаны принадлежать одному множеству. В случае невыполнения этого контракта у результатом будет UB ( могло быть и исулючение, но это накладывает на реализацию жесткие ограничения).
В этом случае, хорошим тоном от разработчиков библиотеки, будет проверить и подсказать в отладочной сборке нарушение контракта. Но это не к коей мере не обработка ошибки, это только подсказка пользователям библиотеки о неправильном использовании.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Ищу работу, 3D, SLAM, computer graphics/vision.
Re: api design: return code or exception - formal criteria
К сожалению очень часто програмисты не отличают и не разделяют ошибки исполнения от состояния объектов или результатов выполнения.
пример результата выполнения.
bool fileExist(...);
Если эта функция вернет false это не означает что произошла ошибка и необходимо прервать выполнение программы.
пример ошибки выполнения.
bool fileExist(...);
Во время выполнения сетевой ресурс на который ссылается путь был отключен, это ошибка выполнения fileExist, и вполнен нормально в таком случае кинуть исключение.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Ищу работу, 3D, SLAM, computer graphics/vision.
Re[14]: api design: return code or exception - formal criter
Здравствуйте, Николай Ивченков, Вы писали:
НИ>Если говорить о типе, которым инстанцируется std::unique_lock, то тут используется именно одна "слабая" концепция Mutex.
Только вот одного инстанцирования не достаточно.
НИ>А то, что такому описанию std::unique_lock можно сопоставить некое эквивалентное по смыслу описание, выраженное через две концепции с более конкретизированными предусловиями — это уже другой вопрос.
Так в чем здесь преимущества "слабой" концепции? Для спецификации методов std::unique_lock по прежнему используется switch по 'типам', а там где не используется — это простое наследование спецификаций.
Как только мы вводим 'шаблоны' спецификаций (а это и есть по сути 'слабая' концепция), наследование, и т.п. — автоматом получаем проблемы, аналогичные проблемам, возникающим при обычном использовании 'типов'. Лишние зависимости, хрупкие/толстые/etc. базовые интерфейсы, нарушения LSP, и т.д.
ЮЖ>>Где оно сформулировано?
НИ>30.4.1.1/3: НИ>
The behavior of a program is undefined if:
НИ>[...]
НИ>— a thread that owns a mutex object calls lock() or try_lock() on that object
НИ>С точки зрения пользователя std::mutex нет никакой разницы между этим undefined behavior и undefined behavior, возникающим в результате несоблюдения условий, которые были бы явным образом специфицированы как предусловия.
17.6.3.11/1:
Violation of the preconditions specified in a function’s Required behavior: paragraph results in undefined
behavior unless the function’s Throws: paragraph specifies throwing an exception when the precondition is
violated.
Согласно этой формулировке — несоблюдение предусловий не всегда UB. В этом ракурсе очень интересно выглядит описание error conditions для mutex::lock. Когда я говорил о "завуалированных" предусловиях — я имел ввиду именно это.
НИ>Т.е. фактически отсутствие владения мьютексом со стороны текущего потока можно считать предусловием для вызова lock.
Хорошо — будем считать.
ЮЖ>>они там, кстати, нарушили свой любимый zero-overhead principle.
НИ>Где именно?
Здесь: "throwing an exception when the precondition is violated" — разрешается диагностировать ситуацию, при которой дальнейшее выполнение приводит к UB и возбуждать исключения. Там где это проиходит и нарушается zero-overhead principle, в частности во всех местах (где "precondition is violated") возбуждения стандартной библиотекой исключений, производных от std::logic_error.
Re[15]: api design: return code or exception - formal criter
Юрий Жмеренецкий:
НИ>>А то, что такому описанию std::unique_lock можно сопоставить некое эквивалентное по смыслу описание, выраженное через две концепции с более конкретизированными предусловиями — это уже другой вопрос.
ЮЖ>Так в чем здесь преимущества "слабой" концепции?
Я не знаю, какие преимущества здесь видели авторы, но факт её применения, IMHO, налицо.
НИ>>С точки зрения пользователя std::mutex нет никакой разницы между этим undefined behavior и undefined behavior, возникающим в результате несоблюдения условий, которые были бы явным образом специфицированы как предусловия.
ЮЖ>
17.6.3.11/1:
ЮЖ>Violation of the preconditions specified in a function’s Required behavior: paragraph results in undefined
ЮЖ>behavior unless the function’s Throws: paragraph specifies throwing an exception when the precondition is
ЮЖ>violated.
ЮЖ>Согласно этой формулировке — несоблюдение предусловий не всегда UB.
Я и не говорил, что несоблюдение предусловий всегда влечёт undefined behavior. Достаточно и того, что иногда undefined behavior приводится как следствие несоблюдения предусловий. Если существуют критерии невозникновения undefined behavior, то с практической точки зрения не имеет никакого значения, обозначает ли документация эти критерии в качестве некоторых предусловий явным образом.
ЮЖ>В этом ракурсе очень интересно выглядит описание error conditions для mutex::lock. Когда я говорил о "завуалированных" предусловиях — я имел ввиду именно это.
Я вообще затрудняюсь сказать, в чём заключается суть "resource_deadlock_would_occur — if the current thread already owns the mutex and is able to detect it" и когда это может быть актуально.
НИ>>Т.е. фактически отсутствие владения мьютексом со стороны текущего потока можно считать предусловием для вызова lock.
ЮЖ>Хорошо — будем считать.
Если считать Lockable (в Boost) завершённой концепцией, то std::mutex и boost::mutex не соответствуют этой концепции, т.к. std::mutex и boost::mutex предъявляют более жёсткие предусловия.
ЮЖ>>>они там, кстати, нарушили свой любимый zero-overhead principle.
НИ>>Где именно?
ЮЖ>Здесь: "throwing an exception when the precondition is violated" — разрешается диагностировать ситуацию, при которой дальнейшее выполнение приводит к UB и возбуждать исключения. Там где это происходит и нарушается zero-overhead principle, в частности во всех местах (где "precondition is violated") возбуждения стандартной библиотекой исключений, производных от std::logic_error.
basic_string(const basic_string<charT,traits,Allocator>& str,
size_type pos, size_type n = npos,
const Allocator& a = Allocator());
Requires: pos <= str.size()
Throws: out_of_range if pos > str.size().
Где тут overhead? IMHO, тут проверка предусловия занимает очень незначительную часть времени от общих затрат на конструирование, так что её трудно назвать overhead-ом. Использование механизма исключений всё равно необходимо, т.к. у аллокатора нет иного способа сообщения об ошибке выделения памяти.
Re[3]: api design: return code or exception - formal criteri
Здравствуйте, minorlogic, Вы писали:
V>> После появились программисты библиотеки, которые поняли, что каждый раз включать галку в студии останавливаться на исключении в отладчике не катит и вставили ассерт перед самим throw. M>Объясните подробнее пожалуйста этот кусок.
Чтобы не перезапускать приложение, которое уже грохнулось на исключении, ставят ассерт перед исключением чтобы вовремя отстановиться в отладчике, обычная практика.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Re[5]: api design: return code or exception - formal criteri
Здравствуйте, Erop, Вы писали:
V>>Для начало можно сделать коды возврата обязательными параметрами всех нужных функций. E>А что это даст, кроме дальнейшего снижения удобства использования библиотеки? E>IMHO, качественный клинтский код -- это забота его авторов. Библиотека должна позволять писать качественный, и не должна мешать это делать или провоцировать на некачественный. Но бороться?
Одна библиотека не сможет этого обеспечить, нужны ещё нормальные программисты, которые её будут правильно использовать. А на практике, обычно, это не выполняется. Поэтому, иногда, лучше не использовать исключения, которые приводят к болоту, а использовать только коды возврата. При этом пользователь не сможет написать рабочий код без проверки кода возврата, что есть защита от дурака.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Re[16]: api design: return code or exception - formal criter
Здравствуйте, Николай Ивченков, Вы писали:
ЮЖ>>>>они там, кстати, нарушили свой любимый zero-overhead principle.
НИ>>>Где именно?
ЮЖ>>Здесь: "throwing an exception when the precondition is violated" — разрешается диагностировать ситуацию, при которой дальнейшее выполнение приводит к UB и возбуждать исключения. Там где это происходит и нарушается zero-overhead principle, в частности во всех местах (где "precondition is violated") возбуждения стандартной библиотекой исключений, производных от std::logic_error.
НИ>
basic_string(const basic_string<charT,traits,Allocator>& str,
НИ> size_type pos, size_type n = npos,
НИ> const Allocator& a = Allocator());
НИ>Requires: pos <= str.size()
НИ>Throws: out_of_range if pos > str.size().
НИ>Где тут overhead? IMHO, тут проверка предусловия занимает очень незначительную часть времени от общих затрат на конструирование, так что её трудно назвать overhead-ом.
Смысл принципа: не хочу — не плачу. Если у меня из контекста использования автоматически вытекает выполнения предусловия — зачем мне нужна проверка "внутри"? На всякий случай? Нарушение в том, что в этом случае у меня нет рычага для ее отключения. А насколько это бьет по производительности — совершенно неважно.
НИ>Использование механизма исключений всё равно необходимо, т.к. у аллокатора нет иного способа сообщения об ошибке выделения памяти.
Ты хочешь сказать что аллокатор кидает out_of_range? Это неверно. Он кидает bad_alloc — и к этому у меня претензий нет. А вот out_of_range к аллокатору не имеет никакого отношения.
Re[2]: api design: return code or exception - formal criteri
Здравствуйте, jazzer, Вы писали:
J>все функции в двух экземплярах, одни бросают, другие (у которых есть параметр boost::system::error_code & ec) — не бросают.
Проще объединить, что и предложили в N2838:
Здравствуйте, byleas, Вы писали:
B>Здравствуйте, jazzer, Вы писали:
J>>все функции в двух экземплярах, одни бросают, другие (у которых есть параметр boost::system::error_code & ec) — не бросают. B>Проще объединить, что и предложили в N2838:
Мы уже с Quasi это обсуждали, я по второму разу не буду