Аннотация:
Если вы пишете на С++, то ваша программа скорее всего состоит из объектов классов, которые в своей совокупности образуют некую систему данных и кода, работающего с этими данныим. И практически всегда вы хотите в какой-то момент сохранить в том или ином виде эти данные — будь то результат многолетних вычислений программы или просто текущее состояние каких-то компонентов системы. А потом снова загрузить эти данные назад, в вашу программу, как будто бы и ничего не происходило. Или искажем отправить эти данные по сети, другой программе. И при этом, очень нехочется тратить много времени на программирование сохранения/загрузки, упаковку структур в какие-то изобретённые сегодня утром форматы, отладку всего этого, модификацию в связи с появлением в структурах данных новых полей, документирование, и прочую головную боль.
Подход, описаный ниже, я надеюсь, поможет многим сэкономить время и облегчить жизнь.
САН>Авторы: САН> Сабельников Андрей Николаевич
САН>Аннотация: САН>Если вы пишете на С++, то ваша программа скорее всего состоит из объектов классов, которые в своей совокупности образуют некую систему данных и кода, работающего с этими данныим. И практически всегда вы хотите в какой-то момент сохранить в том или ином виде эти данные — будь то результат многолетних вычислений программы или просто текущее состояние каких-то компонентов системы. А потом снова загрузить эти данные назад, в вашу программу, как будто бы и ничего не происходило. Или искажем отправить эти данные по сети, другой программе. И при этом, очень нехочется тратить много времени на программирование сохранения/загрузки, упаковку структур в какие-то изобретённые сегодня утром форматы, отладку всего этого, модификацию в связи с появлением в структурах данных новых полей, документирование, и прочую головную боль. САН>Подход, описаный ниже, я надеюсь, поможет многим сэкономить время и облегчить жизнь.
А чем boost::serialize не подошёл?
Каюсь, сам был грешен, использовал собственный велосипед под названием BrokerStorage — использование типа :
В boost то же универсальное описание сериализации для всех классов, но нет промежуточного хранилища.
Делаете простенький переходничёк в свой формат, можете оптимизировать с учётом специфики.
А>Каюсь, сам был грешен, использовал собственный велосипед под названием BrokerStorage — использование типа :
А>MyBrokerStorage <<SomeObject1 ; А>MyBrokerStorage <<SomeObject2 ; А>MyBrokerStorage >> SomeFormat;
А>В boost то же универсальное описание сериализации для всех классов, но нет промежуточного хранилища. А>Делаете простенький переходничёк в свой формат, можете оптимизировать с учётом специфики.
А>Или я чего не понял?
Отвечу под анонимом т.к. пока не получается востановить паролъ.
Во первых — спасибо что просмотрели статью.
Постараюсь коротко и лаконично ответить на Ваш вопрос.
Сразу оговорюсь о терминологии — "хранилище"(в статье) и "архив"(в бусте) — обозначают одну и ту же сущность.
Что касается промежуточного хранилища — те же яйца только в профиль
Продемонстрированный в статье подход не обязывает вас использовать так называемый "промежуточный контейнер"(а именно вы наверное имели в виду временно загружаемые в память данные) — например в случае сериализации в реестр, никакие промежуточные данные не хранятся — все операции делигируются сразу в API операционной системы. Если вы хотите сохранять в сплошной кусок памяти именно моей реализации — тогда да, моя реализация хранилища(тот же архив — вид сбоку) предпологает раскручиваение древовидной структуры данных из упакованных структур в stl-ые ассоциативные массивы, что бы доступ по именам полей был быстрее и проще реализован.
Вы, если пожелаете, можете писать свою реализацию, оптимизируя по скорости/размеру если это актуально. Это вопрос организации хранилища(в бусте это обозначается терминов "архив").
А кроме того, точно так же в бусте вы создаёте "промежуточный экземпляр" архива:
//Пример из документации по бусту с http://www.boost.org/libs/serialization/doc/index.html
std::ifstream ifs("filename", std::ios::binary);//Создаётся провайдер потока данных - в данном случае из файлаboost::archive::text_iarchive ia(ifs);//Создаётся такой же промежуточный объект-хранилище
// read class state from archive
ia >> newg;//Загрузка данных из архива
Отсюда позвольте перейти к сути Вашего вопроса. Почему не boost::serialization ?.
В бусте сохраняемые объекты представляются последовательным потоком. Т.е. там имеет значение последовательность элементов описанных в функции serialize(*).
В какой последовательности засунуты — в такой и высовываем. Как тока появляются разные версии, начинается самодеятельность с условными операторами:
//То же из примера с http://www.boost.org/libs/serialization/doc/index.html template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
// only save/load driver_name for newer archivesif(version > 0)
ar & driver_name;
ar & stops;
}
А я нехочу тратить время на постоянное согласовывание форматов версий.
Ну и кроме того, чисто субъективно:
a) мне не нравится когда простые операторы в с++ значат нетривиальные вещи: оператор "&" перегружен, и вызывает "<<" или ">>" хотя на первый взгляд выглядит как объявление ссылки(без инициализации правда ).
b) мне больше нравится писать\смотреть карту сериализации, чем функцию — легче писать, легче читать и видеть иерархию, минуя путанницу условных операторов согласования версий.
ИМХО основной недостаток статьи — нераскрытие того, как всё же [де]сериализовать объекты по указателю [в контейнерах].
Re[2]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
08.07.06 21:06
Оценка:
Здравствуйте, siv, Вы писали:
siv>ИМХО основной недостаток статьи — нераскрытие того, как всё же [де]сериализовать объекты по указателю [в контейнерах].
А разве это не очевидно ?
N_SERIALIZE_POD (*m_pguid_val, "m_guid_val")// Если у вас указатель - разыменовываем
Это то, что мы всегда делаем, когда у нас есть указатель а нужна ссылка.
Кроме того, если вы не уверенны, что Ваш указатель валиден, можно так:
А>А разве это не очевидно ?
Вопрос был с подвохом
А>
А>N_SERIALIZE_POD (*m_pguid_val, "m_guid_val")// Если у вас указатель - разыменовываем
А>
Ok, этим мы запишем в "поток" содержимое объекта, на который указывал указатель в момент сериализации.
Теперь при десирализации мы должны сделать следующее (грубо):
— выделить память и сконструировать объект;
— десериализовать данный объект из "потока";
— если все Ok, проинициализировать член-указатель на готовый объект.
Т.о. имеется некоторая потребность в макросах типа
N_SERIALIZE_PTR_POD, N_SERIALIZE_PTR_T .
Но! Довольно часто встречается ситуция, когда на один и тот же объект имеются указатели из разных сериализуемых контейнеров. Следовательно приемлемый механизм сериализации должен предлагать механизмы по удобной (прозрачной) обработке подобных вещей. Например, сериализация в MFC делает это ценой некоторых накладных расходов...
В данной статье я и не увидел ничего, относящегося к вышеизложенному.
А>Это то, что мы всегда делаем, когда у нас есть указатель а нужна ссылка.
А>Кроме того, если вы не уверенны, что Ваш указатель валиден, можно так: А>
Не понял, каким образом это помогает установить валидность указателя
Re[4]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
09.07.06 19:34
Оценка:
Здравствуйте, siv, Вы писали:
А>>А разве это не очевидно ? siv>Вопрос был с подвохом
А>>
А>>N_SERIALIZE_POD (*m_pguid_val, "m_guid_val")// Если у вас указатель - разыменовываем
А>>
siv>Ok, этим мы запишем в "поток" содержимое объекта, на который указывал указатель в момент сериализации. siv>Теперь при десирализации мы должны сделать следующее (грубо): siv>- выделить память и сконструировать объект;
это ещё зачем ? можно десериализовывать в объект который уже существует на момент десериализации. siv>- десериализовать данный объект из "потока"; siv>- если все Ok, проинициализировать член-указатель на готовый объект. siv>Т.о. имеется некоторая потребность в макросах типа siv>N_SERIALIZE_PTR_POD, N_SERIALIZE_PTR_T .
Так к сожалению и непонял необходимость этих макросов
siv>Но! Довольно часто встречается ситуция, когда на один и тот же объект имеются указатели из разных сериализуемых контейнеров. Следовательно приемлемый механизм сериализации должен предлагать механизмы по удобной (прозрачной) обработке подобных вещей. Например, сериализация в MFC делает это ценой некоторых накладных расходов... siv>В данной статье я и не увидел ничего, относящегося к вышеизложенному.
Если у Вас в проекте ситуация с указателями на один и тот же объект из разных классов — определитесь, кто из них владеет объектом и включите в карту сериализации объект один раз именно там. В остальных классах он не должен входить в карту сериализации. IMHO это во первых на много лучше с точки зрения дизайна. Во вторых — такой интеллектуальный анализ, о котором Вы говорите, добавит избыточную сложность в описываемый подход.
А про сериализацию в MFC я вообще, даже разговаривать нехочу
А>>Это то, что мы всегда делаем, когда у нас есть указатель а нужна ссылка.
А>>Кроме того, если вы не уверенны, что Ваш указатель валиден, можно так: А>>
siv>Не понял, каким образом это помогает установить валидность указателя
А самым, что ни на есть, элементарным — проверка на ноль. Если указатель куда-то указывает — щитаем что он валиден.
Доза паранои в работе с указателями никогда не повредит (случай когда у вас остаётся указатель, а сам объект уже удалён или когда указатель неинициализирован не рассматривается, т.к. это просто плохой стиль — сериализация тут непричём)
А>это ещё зачем ? можно десериализовывать в объект который уже существует на момент десериализации.
И каким волшебным образом он вдруг "уже существует".
Представь, у тебя в классе ну, например std::list< CMyClass*> m_container. Его нужно [де]сериализовать также удобно, как и остальные члены (т.е. с помощью карты и макросов).
В статье о сериализации подобного скромно умалчивается.
siv>>N_SERIALIZE_PTR_POD, N_SERIALIZE_PTR_T . А>Так к сожалению и непонял необходимость этих макросов
Я забыл упомянуть еще эти:
N_SERIALIZE_STL_CONTAINER_PTR_POD, N_SERIALIZE_STL_CONTAINER_PTR_T...
Это не необходимость, а дополнительное удобство, делающее твоё решение более завершенным и готовым к употреблению
siv>>Но! Довольно часто встречается ситуция, когда на один и тот же объект имеются указатели из разных сериализуемых контейнеров. Следовательно приемлемый механизм сериализации должен предлагать механизмы по удобной (прозрачной) обработке подобных вещей. Например, сериализация в MFC делает это ценой некоторых накладных расходов... siv>>В данной статье я и не увидел ничего, относящегося к вышеизложенному. А>Если у Вас в проекте ситуация с указателями на один и тот же объект из разных классов — определитесь, кто из них А>владеет объектом и включите в карту сериализации объект один раз именно там.
Даже не рискну предположить, что ты о смарт-пойнтерах не слышал
Данный совет выглядит как-то уж слишком по-детски и совсем не решает проблемы.
Разные объекты одного и того же класса могут равноправно "шарить" одни и те же экземпляры объектов.
И как твой совет помогает восстановить такой граф (e.g. DAG) после десериализации?
А> В остальных классах он не должен входить в карту сериализации. IMHO это во первых на много лучше с точки зрения дизайна.
Дизайны разные бывают, что где-то лучше, в другом месте оно м.б. неприемлемо
А>Во вторых — такой интеллектуальный анализ, о котором Вы говорите, добавит избыточную сложность в описываемый подход.
Авторы MFC имели другое мнение...
А>А про сериализацию в MFC я вообще, даже разговаривать нехочу
Ну, на том и закончим.
MFC же я упомянул только как один из примеров реализации того аспекта, который отсутсвует в твоей имплементации Yet Another Serialization.
Re[6]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
10.07.06 11:40
Оценка:
Здравствуйте, siv, Вы писали:
А>>это ещё зачем ? можно десериализовывать в объект который уже существует на момент десериализации. siv>И каким волшебным образом он вдруг "уже существует". siv>Представь, у тебя в классе ну, например std::list< CMyClass*> m_container. Его нужно [де]сериализовать также удобно, как и остальные члены (т.е. с помощью карты и макросов). siv>В статье о сериализации подобного скромно умалчивается.
Давайте разберёмся — сначала вы говорили о членах указателях, теперь о члене контейнере который хранит указатели. А это ужо другое. Скорее всего в списке хранятся указатели на базовый тип полиморфного объекта, даже если мы сможем сохранить данные из такого списка, через виртуальную функцию, каким образом мы сможем сконструировать объект при десериализации ? Тут появляется комбинаторное множество вариантов, для каждого из которых нужна будет специальная реализация. Если творить очередного монстра, то можно конечно связаться с пораждающими паттернами, связывать их с типами в Вашем проекте....но это IMHO просто ненужно в библиотеке сериализации — это можно сделать руками внутри вашего проекта, написать функцию сохранения\загрузки имея информацию о действительных о типах объектов.
siv>>>N_SERIALIZE_PTR_POD, N_SERIALIZE_PTR_T . А>>Так к сожалению и непонял необходимость этих макросов siv>Я забыл упомянуть еще эти: siv>N_SERIALIZE_STL_CONTAINER_PTR_POD, N_SERIALIZE_STL_CONTAINER_PTR_T... siv>Это не необходимость, а дополнительное удобство, делающее твоё решение более завершенным и готовым к употреблению
И настолько тяжеловесным, непонятным и непрозрачным, что никому не интересным
...... siv>Даже не рискну предположить, что ты о смарт-пойнтерах не слышал siv>Данный совет выглядит как-то уж слишком по-детски и совсем не решает проблемы. siv>Разные объекты одного и того же класса могут равноправно "шарить" одни и те же экземпляры объектов. siv>И как твой совет помогает восстановить такой граф (e.g. DAG) после десериализации?
Никак. А>> В остальных классах он не должен входить в карту сериализации. IMHO это во первых на много лучше с точки зрения дизайна. siv>Дизайны разные бывают, что где-то лучше, в другом месте оно м.б. неприемлемо
А>>Во вторых — такой интеллектуальный анализ, о котором Вы говорите, добавит избыточную сложность в описываемый подход. siv>Авторы MFC имели другое мнение...
А>>А про сериализацию в MFC я вообще, даже разговаривать нехочу siv>Ну, на том и закончим. siv>MFC же я упомянул только как один из примеров реализации того аспекта, который отсутсвует в твоей имплементации Yet Another Serialization.
MFC — не самый удачный пример В комапнии, в которй я сейчас работаю, у многих людей упоминание MFC вообще вызывает нервозность, в том числе эти ассоциации связаны с опытом использования архивов.
Уважаемый siv, Вы правы кое в чём — библиотека не умеет конструировать полиморфные объекты, из-за того, что не хранится никакой избыточной инофрмации и из-за того, что такая сложность нужна в редких случаях. Я решил исполнить максимально простую легковесную реализацию, что бы покрыть 90% потребностей большинства программистов.
С другой стороны я уверен что сериализация в MFC тоже не панацея от всего, и врятли можно найти библиотеку подходящую для всех типов задач.
Тем не мене спасибо что с интересом отнеслись к статье.
Здравствуйте, Аноним, Вы писали:
siv>>Теперь при десирализации мы должны сделать следующее (грубо): siv>>- выделить память и сконструировать объект; siv>>- десериализовать данный объект из "потока"; siv>>- если все Ok, проинициализировать член-указатель на готовый объект. siv>>Т.о. имеется некоторая потребность в макросах типа siv>>N_SERIALIZE_PTR_POD, N_SERIALIZE_PTR_T .
А>это ещё зачем ? можно десериализовывать в объект который уже существует на момент десериализации. А>Так к сожалению и непонял необходимость этих макросов
Это одно из стандартных требований к маханизму сериализации ( must have )
Необходимо для того, чтобы иметь возможность восстановить связи между объектами (указатели) в прочитанном наборе объектов(модели),
а также для динамического создания объектов в куче. Фактически, не имея данной возможности, механизм оказывается
неспособным сохранять/восстанавливать объекты, связанные с другими объектами. В упомянутых boost и MFC это реализовано.
Простые примеры:
// Пример1: Сохранить/восстановить объект класса Container, при условии что он может содержать объекты класса Base и объекты класса Derived?class Base { int x; };
class Derived : public Base { int y; };
class Container { std::vector<Base*> m_items; };
// Пример2: Сохранить p, а потом восстановить ("ссылка на родителя")?class Child { Parent* parent; };
class Parent { Child child; };
Parent p;
p.child->parent = &p;
siv>>Но! Довольно часто встречается ситуция, когда на один и тот же объект имеются указатели из разных сериализуемых контейнеров. Следовательно приемлемый механизм сериализации должен предлагать механизмы по удобной (прозрачной) обработке подобных вещей. Например, сериализация в MFC делает это ценой некоторых накладных расходов... siv>>В данной статье я и не увидел ничего, относящегося к вышеизложенному.
+1
А>Если у Вас в проекте ситуация с указателями на один и тот же объект из разных классов — определитесь, кто из них владеет объектом и включите в карту сериализации объект один раз именно там. В остальных классах он не должен входить в карту сериализации. IMHO это во первых на много лучше с точки зрения дизайна. Во вторых — такой интеллектуальный анализ, о котором Вы говорите, добавит избыточную сложность в описываемый подход.
К сожалению, это не всегда возможно. Точнее, это, как правило, возможно только для достаточно простых систем..
Если бы все было так просто, люди не придумывали бы всяких "умных указателей" и "сборщиков мусора".
IMHO, вообще, механизм, который не умеет восстанавливать связи между объектами (указатели), не стоит называть "сериализаций"...
А>А про сериализацию в MFC я вообще, даже разговаривать нехочу
Почему же?
Еще камешки. Все IMHO.
1. Ваши макросы, кажется, не работают для std::set, std::map (ассоциативных контейнеров).
2. push_back — не самая светлая идея для десериализации векторов... Это (а) медленно и (б) расточительно. resize() + индексация будет получше..
3. К чему все эти заморочки с разными типами строк ?!
Такая вот критика
Re[6]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
10.07.06 13:54
Оценка:
Спасибо, bnk, ты написал именно то, что я хотел сказать в соседнем мессадже, только ещё лучше
bnk>Еще камешки. Все IMHO.
Я добавлю еще один кирпичик.
Именование сохраняемых членов — фича понятная, в смысле версионности.
Но, ИМХО, довольно избыточная по объему сохраняемых данных.
Т.е. "КПД" может оказаться неприемлемо низким...
В общем предложенный механизм — не панацея
Re[7]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
10.07.06 13:56
Оценка:
А>Спасибо, bnk, ты написал именно то, что я хотел сказать в соседнем мессадже, только ещё лучше
Данный Аноним — это siv.
А>Давайте разберёмся — сначала вы говорили о членах указателях, теперь о члене контейнере который хранит указатели. А это ужо другое.
Сначала я намекал на недостатки статьи. потом усугубил. Сути это не меняет.
А> Скорее всего в списке хранятся указатели на базовый тип полиморфного объекта, даже если мы сможем сохранить данные из такого списка, через виртуальную функцию, каким образом мы сможем сконструировать объект при десериализации ? Тут появляется комбинаторное множество вариантов, для каждого из которых нужна будет специальная реализация. Если творить очередного монстра, то можно конечно связаться с пораждающими паттернами, связывать их с типами в Вашем проекте....но это IMHO просто ненужно в библиотеке сериализации — это можно сделать руками внутри вашего проекта, написать функцию сохранения\загрузки имея информацию о действительных о типах объектов.
В списке могут храниться указатели и на конкретный класс и на базовый. Не суть.
Важно то, что предложенный механизм не решает данную проблему НИКАК.
MFC и boost решают.
siv>>Это не необходимость, а дополнительное удобство, делающее твоё решение более завершенным и готовым к употреблению А>И настолько тяжеловесным, непонятным и непрозрачным, что никому не интересным
Уже одного меня достаточно, чтобы слово "никому" было здесь не уместно
А>MFC — не самый удачный пример В комапнии, в которй я сейчас работаю, у многих людей упоминание MFC вообще вызывает нервозность, в том числе эти ассоциации связаны с опытом использования архивов.
Это не аргумент. В компании, на которую я сейчас работаю, испорльзование boost не разрешено И что?
И вообще, подобные заявления о нервозности я уже много раз слышал и о MFC и о COM и о C++ и о шаблонах и об указателях и о garbage collector...
Не удивлюсь, что скоро услышу подобное о .Net, С# 1.0, потом 2.0, окошках без 3D наворотов, ЭЛТ мониторах, мышах с ball и пр.
А>Уважаемый siv, Вы правы кое в чём
Угу, на абсолют никогда не претендовал И давайте по форумски на "ты", Ок?
А> — библиотека не умеет конструировать полиморфные объекты, из-за того, что не хранится никакой избыточной инофрмации и из-за того, что такая сложность нужна в редких случаях. Я решил исполнить максимально простую легковесную реализацию, что бы покрыть 90% потребностей большинства программистов.
Да, вот именно за легковестность я и поставил статье +1
А насчет редкости таких случаев — не согласен. У меня, как раз, наоборот.
А>С другой стороны я уверен что сериализация в MFC тоже не панацея от всего, и врятли можно найти библиотеку подходящую для всех типов задач.
Есссно!
Но уж если сериализовать, то сериализовать до конца! Т.е. и по указателям тоже must have.
Re[8]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
10.07.06 14:49
Оценка:
Здравствуйте, siv, Вы писали:
...... siv>В списке могут храниться указатели и на конкретный класс и на базовый. Не суть. siv>Важно то, что предложенный механизм не решает данную проблему НИКАК. siv>MFC и boost решают.
Я кстати не в курсе, каким образом буст решает данную проблему ? Мне просто интересно, если вас незатруднить пояснить.
siv>Это не аргумент. В компании, на которую я сейчас работаю, испорльзование boost не разрешено И что?
Вы не поверите, у нас тоже нельзя было но потом таки удалось убедить руководство использоватьв некоторых проектах.
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, siv, Вы писали:
А>...... siv>>В списке могут храниться указатели и на конкретный класс и на базовый. Не суть. siv>>Важно то, что предложенный механизм не решает данную проблему НИКАК. siv>>MFC и boost решают. А>Я кстати не в курсе, каким образом буст решает данную проблему ? Мне просто интересно, если вас незатруднить пояснить.
Принцип один, что в boost, что в MFC..
Во-первых, составляется "реестр известрых науке классов", с функциями генерации объектов. В MFC это делается с помощью CRunTimeClass, в boost- через шаблоны.
При сохранении определяется ТОЧНЫЙ класс записываемого полиморфного объекта (в MFC — через тот же CRunTimeClass, в boost вроде через typeid),
и в файл записывается некая ссылка на него ("GUID"). После этой "преамбулы" выполняется собственно сохранение полей класса.
При чтении, читается "преамбула", по "GUID"-у определяется, что за объект мы прочитали, находится в "реестре" соответствующий ему класс,
и вызывается соответствующая функция для создания объекта. После этого вызывается загрузка полей для даннго класса.
как-то примерно так...
с указателями на объекты есть свои заморочки (а особенно с "умными", типа boost::shared_ptr)... В общем, лучше смотреть оригинал Serializable Concept
Re[6]: Ещё один подход к сериализации на С++
От:
Аноним
Дата:
10.07.06 20:05
Оценка:
Здравствуйте, bnk, Вы писали:
......<skiped>...... bnk>1. Ваши макросы, кажется, не работают для std::set, std::map (ассоциативных контейнеров).
Не работают — об этом написанно в статье.(не работают т.к. не представляется возможным написать единообразную функцию работы с последовательными контейнерами и с ассоциативными), хотя было бы удобным иметь такую возможность. Возможно я придумаю на этот счёт что-то в дальнейшем. bnk>2. push_back — не самая светлая идея для десериализации векторов... Это (а) медленно и (б) расточительно. resize() + индексация будет получше..
Согласен, но это будет получше только для векторов — для списка это не скомпилируется, а делать частичные специализации — это поиметь проблемы с VC6. К тому же эта вечная страсть всё оптимизировать там где это нужно и ненужно часто только пустая трата времени(не всегда конечно), т.к. выигрыш в производительности настолько незаметный что никто и неоченит . bnk>3. К чему все эти заморочки с разными типами строк ?!
К тому, что функции доступа\присваивания у них разные(у stl и у ATL\WTL\MFC), а перегрузить я немогу т.к. внесу ненужные дополнительные зависимости типов.
bnk>Такая вот критика
Спасибо