Как стать автором
Обновить

Комментарии 71

если пишите API то про классы и slt/boost надо забыть. только struct и POD типы
Чем вам STL не угодил? Он же входит в стандарт C++.
Возможно речь шла о том, что у C++ Standard Library нет (пока) стабильного ABI. Но как-то уж очень категорично
А про аббревиатуру POD стоит уже забыть, как тут уже несколько раз заметили, 2014 на дворе.
Ну давайте следить за тем, что и как поддерживают gcc и MS; поддерживать редкими патчами то, что в других компиляторах не работает. Зачем забывать-то?
Затем, что они свято уверены, что ничего, кроме MSVC в природе нету.

Раз Microsoft сказал не использовать STL, а предложил использовать COM — значит все переходим на COM. И то, что на всех остальных платформах этой проблемы нету (даже на Windows при использовании других компиляторов типа MinGW!) никого не волнует. «Сказали в сад — значит в сад».
Все бы хорошо, вот только половина библиотек и продуктов, в том числе open source не хочет собираться mingw. Под виндой им подавай MSVС, или развлекайся с компиляцией.
Если вам действительно нужны все эти библиотеки — тогда да, вопросов нету, но на Winows свет клином не сошёлся, так что во многих случаях можно без поддержки MSVC обойтись. Или использовать для сборки всех библиотек одну конкретную версию MSVC и разделяемую стандартную библиотеку — в этом случае тоже всё работает.
Если вы пишете server-side, то про винду и вообще кроссплатформенность можно и не упоминать. Ваш продукт будет работать на вашем кластере, который вы полностью контролируете. Если речь заходит о клиентском приложении, то с этой проблемой вы, к сожалению, рано или поздно столкнетесь.
Можно поставлять клиентам библиотеки, скомпилированные несколькими версиями компилятора, например VS 2010, VS 2012 и VS 2013. Это может уменьшить, а чаще свести к нулю, проблемы из-за незнания компиляторами от Microsoft понятия совместимость ABI.
Неформально в юникс системах для плюсов принято использовать itanium c++ abi.
заверните API в dll и вызовите в другом языке. Да что там — в другой версии компилятора — здравствуйте эксепшены и утечки памяти
Для plain C есть специальная техника работы через handle объекта (указатель на forward declared struct, у которой единственный член — объект оборачиваемого класса, сама структура описывается в .cpp, где уже можно использовать #include C++ классов) и дублирование методов экспортируемыми extern «C» функциями. В целом в других языках есть удобные биндинги: JNI, MC++, Boost.Python, Rice и т.п., но если совсем край и надо чистый Си, то нужно для Си делать API отдельно в рамках wrapping-процесса, тут уж придётся делать downgrade типов.
--то нужно для Си делать API отдельно в рамках wrapping-процесса, тут уж придётся делать downgrade типов

в реальной жизни api делают и ставляют на года. и потом сертифицируют и запрещают менять.

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

И около 3 лет нам не давали новее версии. Потому что при смене версии надо снова сертифицировать весь софт.

Я сменил три версии Visual Studio — представте что бы было будь там STL наружу. Да и никто бы не позволил.
И как там в каменном веке поживается?
Ну без классов умных указателей, потоков с мьютексами, без стандартных контейнеров массивов, множества, маппинга, да даже без всем известного базового исключения будет как-то трудно разрабатывать понятную библиотеку. К тому же что будем кидать в виде исключения, throw 1, если только struct и POD-типы? Для языка Си можно сделать отдельный интерфейс, обернув данные в структуры с forward declaration в .h (с обычным вложением объекта как единственного поля в структуре в .cpp-файле) и продублировав методы аналогичными extern «C» функциями, но это исключительно для случаев когда нужен чистый Си и для него отдельно выносится API. Я понимаю что любой STL класс можно реализовать самому, но стандартные реализации довольно хороши и всем известны, STL есть везде, чем вам не нравится использование std:: классов?
--К тому же что будем кидать в виде исключения, throw 1, если только struct и POD-типы?

евенты: с таким функтором можно делать логгирование и даже прогресс бар в приложении

typedef ptrdiff_t (*pfnMsg)( int logLevel, int barNumber, int resourceId, int position
, const wchar_t *wzMessage, const char *szFuncName, const int lineNumber, ptrdiff_t pCustomObject );
Хм… вы там что-то про POD'ы и структуры рассказывали и тутже предлагаете использовать wchar_t: тип, переносимость которого между разными компиляторами не гарантируется…
Я так понимаю, речь шла про «показ наружу». Внутри то, конечно, можно использовать все, что угодно (slt/boost), а снаружи должны быть видны только pod'ы. Не понимаю почему человека заминусовали.
Это, наверно, правильный подход — когда наружу торчат интерфейся только из POD'ов и структур. Но как показывает практика, при таком подходе к архитектуре интерфейса получается так, что пользователь использует удобные ему фичи(да хоть тотже STL с его контейнерами, смарт-пойнтерами и тд и тп), а потом ему надо вызвать что-то из вашей библиотеки и он начинает говнокодить, реализуя прослойку между своим приложением и вашей библиотекой. И хорошо, если можно преобразовать типы на той же памяти(например, дернув сырой указатель из std::vector), а то может потребоваться перепаковывать память(например, приводить std::list к void*) и просядет производительность приложения.
Ну тот же std::string вполне можно вернуть, не возвращать же const char*. Или с std::vector или std::deque работать, не принимать же странный element* с количеством отдельно. Совсем уж до POD-типов зачем себя ограничивать. Тот же std::map или std::set замучаешься сам реализовывать. Вряд ли кто-то в свободное время балуется реализацией красно-чёрных деревьев или например тот же std::unordered_map, его вообще проблематично заменить.
О том и речь, что даже std::string нельзя возвращать, если библиотека распространяется в скомпилированном виде и претендует на совместимость.

Представьте, что у вас (под Windows) есть динамическая библиотека, возвращающая ссылку на экземпляр такого типа:
struct {
std::string get_string() const { return «abc»; }
}
Допустим, библиотека скомпилирована с помощью msvc++.
При использовании библиотеки в программе, собранной другим компилятором, получаем undefined behaviour (почти наверняка, поскольку внутреннее представление строки может быть другим).
Причина — отсутствие наперед определенного стабильного ABI у std::string.
Причина — отсутствие наперед определенного стабильного ABI у std::string.

Стабильный ABI отсутствует не у std::string, а у компиляторов msvc++ и стандартной библиотеки из msvc++. Не надо переносить проблемы msvc++ на другие компиляторы, где стабильный ABI существует уже много лет.

Для msvc++, возможным решением будет поставка сборок библиотек сразу под несколько версий msvc++, все равно другие компиляторы на платформе Windows распространены очень мало.
Как сказать, интеловый компилятор довольно популярен под Windows.
У него нет своего рантайма С++ и он использует рантайм MSVC++, а также бинарно совместим с MSVC.
Для API классы и STL/Boost — это совершенно нормально.

Проблемы начинаются с ABI. То есть там, где клиентом библиотеки могут быть не плюсы.
Не обязательно другие языки, другой компилятор, другая версия того же компилятора, да даже другой набор флагов компилятора могут дать бинарно-несовместимые результаты.
Ну в принципе да. Я как-то привык уже думать о плюсовых библиотеках как о коде, который собирается под данный конкретный компилятор, и линкуется статически (все равно там обычно куча шаблонов в заголовках, а то и все).
данные можно объявить отдельным классом без реализации в заголовочном файле и вынести его целиком в .cpp-файл с реализацией методов

Думаю, стоит упомянуть что такой паттерн называется Pimpl.
Не совсем, речь идёт о разделении данных и интерфейса. К тому же мне не нравится подход описанный как Pimpl (указатель на реализацию), если говорить о том, что в С++ в .cpp выносится имплементация, то она и так выносится в .cpp-файл, для того и нужно разделение .h/.cpp. Подход переноса описания данных в реализацию и разделения именно данных от интерфейса класса мне ближе, он берёт своё начало в старом-добром языке Си, когда структура описывалась forward declatation и использовалась как handle сродни this в С++, т.е. передавалась первым параметром в функции, который работали на манер методов в С++. Для примера можно посмотреть API WinPCAP… Как видите я везде избегаю нелюбимого мной понятия impl и данные класса описал именно как данные: class data. Да и copy_on_write шаблон интуитивно понятно что он работает с данными класса, а не с его реализацией, копируется же не реализация, а данные объекта.
Если быть точнее. то это пример технологии D-Pointer, который конечно же является частным случаем Pimpl.
С++ уже низкоуровневый… эххх, снова преамбула холиварная.
НЛО прилетело и опубликовало эту надпись здесь
Для середнячка как раз статья нужная, поскольку часто они работают в команде при разработке серьёзной либы, а иногда пишут что-то своё в порыве альтруизма на волне своих локальных успехов. Уж извините, что не угодил продвинутому «прогеру», но для PRO-уровня писать дело неблагодарное, аудитория узкая и перманентно враждебно настроенная. В любом случае спасибо что снизошли до уровня убогого автора, пишущего никчёмные статьи. Я старался как мог сделать статью и понятно простой и полезной широкой аудитории. Увы мне, если не вышло.
НЛО прилетело и опубликовало эту надпись здесь
Ну лет 5-7 назад мне бы такая статья не помешала бы.
НЛО прилетело и опубликовало эту надпись здесь
Вот вы и сами сказали, что среднячки этого не знают, а про — знают. Соответственно, статья нужна тем среднякам, которые в будущем вырастут в про.
Хотелось бы добавить, что в Qt для вышеописанного есть удобные готовые средства. Они используются в самом коде Qt и могут использоваться при написании своих библиотек. См. d-pointer, QSharedDataPointer.
Поддерживаю, Qt отличная библиотека не только сама по себе, но и как набор инструментов для написания своих библиотек. Но увы, иногда завязка на Qt бывает избыточной.
По поводу самой статьи.
Использовать shared_ptr во всех класс библиотеки, если она будет применяться только в проектах на C++ — ошибка
1) Слишком дорогое удовольствие.
2) Непривычное поведение, т.к. в стандартном случае передается указатель, если программист хочет «расшарить» данные

А так, хотите писать хорошой API — берите соглашения, принятые в Qt и следуйте им.
Я тоже сперва так думал, позже оказалось что я экономил на спичках, так что std::shared_ptr можно спокойно копировать, в большинстве случаев 99% времени съедают сетевое взаимодействие (с той же БД или удалёнными серверами с бизнес-логикой по RPC) либо какие-нибудь адовые алгоритмы коллег по цеху. В C++ не хватает кстати асинхронности и транзакционности данных, но в целом при должном умении на C++ можно создать систему любой сложности с безграничными возможностями для оптимизации. Что до частых выделений памяти и её фрагментации, то с этим можно бороться выделяя общие сущности, размещая в них более мелкие, так например для полей выборки в каждой записи можно через placement new разместить все данные результата запроса, это сэкономит время на выделение на порядок меньшего числа памяти, но в целом сами записи в выборке видимо придётся хранить именно по ссылке на данные через shared_ptr или copy_on_write, просто потому что на бизнес-логике часто идёт дообработка выборки с базы данных. Это по первому пункту.
По второму: поведение ничем не отличается от стандартного, разве что копирование произойдёт позже. Заставлять разработчика оперировать указателями, имхо плохая практика, если это обычные указатели, любые операции с ними небезопасны, если это указатели умные, то операции с ними громоздки и лучше всю дополнительную работу с умными указателями сразу завернуть в класс, с которым пользователь будет работать уже без ошибок.
Если
в большинстве случаев 99% времени съедают сетевое взаимодействие (с той же БД или удалёнными серверами с бизнес-логикой по RPC) либо какие-нибудь адовые алгоритмы коллег по цеху
, то зачем вы вообще на C++ пишете? Чтоб помучаться?
Потому что нужна максимальная скорость на критичных участках. Но это не отменяет того факта, что пользователю системы нужно удобное API, вне зависимости от того, что твориться в движке. Это как автомобиль. Машина должна быть красивой, хорошо ездить, удобно управляться и не должна требовать каких-то навыков сверх навыка вождения.
Ну так и написать тогда на C++ только критичные участки, а остальное на том же питоне, например. И API На питоне в том числе.
Стоп-стоп-стоп, что это за критичные участки, которые занимают 1% от выполнения?
Давайте сначала разберемся с этим, а потом пойдем дальше.

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

Ну а в довесок всего этого приходится выдумывать свой сборщик мусора и свои средства управления памяти. Эти велосипеды вам точно нужны?
Может проще заморочиться со статистической оптимизацией в C# и Java?

У C++ есть определённые преимущества в при разработке системного кода.
— Идиома RAII, обеспечивающая детерминированное управление любыми ресурсами (не только памяти).
— Шаблоны, обеспечивающие zero-cost абстракции.
— Быстрый старт приложения.
— Как правило, существенно меньшее потребление памяти.
— Возможность использовать низкоуровневые платформо-зависимые примитивы (к счастью, обёртки над ними постепенно проникают в «высокоуровневые» языки).

> если мы не используем обычные указатели нафига гам С++?
Использовать сырые указатели обычно нет необходимости. Часто больше подходят итераторы и умные указатели.

> Стоп-стоп-стоп, что это за критичные участки, которые занимают 1% от выполнения?
Может, 1% и обуслен реализацией на соответствующем языке? Возможно, при реализации на Python соотношение было бы совсем другим.
>У C++ есть определённые преимущества в при разработке системного кода.
Собственно я про первые два пункта и сказал. Кстати, повсеместное использование shared_ptr вставляет палки в детерминированность.
>Быстрый старт приложения.
Это точно про API для высокровневых языков?


>Использовать сырые указатели обычно нет необходимости. Часто больше подходят итераторы и умные указатели.
Итераторы это хорошо, но итераторы есть и в других более высокуровневых язык. А, напомню, приложение, уже написано не на C++.

>Может, 1% и обуслен реализацией на соответствующем языке? Возможно, при реализации на Python соотношение было бы совсем другим.
Возможно, но ЧТО?
> использование shared_ptr вставляет палки в детерминированность

Абсолютно верно, глубокие деревья объектов могут приводить к задержкам при освобождении ресурсов. Их стоит рассматривать как зло, иногда необходимое. Sean Parent вообще считает std::shared_ptr<T> современным аналогом глобальных переменных.
COW лично я тоже не особо приветствую, да и дизайн Qt весьма далёк от совершенства.

> Это точно про API для высокровневых языков?
Не понял мысли. Речь вроде шла о том, зачем связываться с C++ без сырых указателей, если есть «высокоуровневые» языки со сборщиком мусора.

> итераторы есть и в других более высокуровневых язык.
Там совсем другие итераторы, они проще, но тяжеловесней. В C++ алгоритмы на итераторах превращаются в компактные и быстрые циклы без виртуальных вызовов. По сути это тонкая прослойка, которую компилятор отбрасывает, превращая в сырые указатели.

Кстати, ещё один весомый довод в пользу системного программирования на C++:
— Отсутствие серьёзных задержек при сборке мусора, меньшая latency.

Ну и C++11 — гораздо более приятный для повседневного использования язык, чем был C++98. Комитет развивает сильные стороны языка в правильном направлении, не ломая при этом обратной совместимости. Это не может не радовать.
>Речь вроде шла о том, зачем связываться с C++ без сырых указателей, если есть «высокоуровневые» языки со сборщиком мусора.
Ну у нас статья об API. И ситуация такова: народ уже пишет прогу на Java, Python и т.д, но часть пишется на плюсах.

Так вот, если мы выкинули из употребления сырые указатели, перешли на shared_ptr, то ради чего мы вносим в уже реализующийся проект доп.сложности? Стоит ли оно того?
народ уже пишет прогу на Java, Python и т.д, но часть пишется на плюсах. Стоит ли оно того?

Автор работает в области GameDev, я думаю, C++ там — весьма разумный выбор.

выкинули из употребления сырые указатели

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

перешли на shared_ptr

Вовсе не обязательно переходить на shared_ptr. Да и не обязательно вообще начинать с сырых указателей. C++ наиболее логичен и производителен в тех случаях, когда мы используем value-семантику, которой буквально пропитан дизайн языка. А если где-то удобно воспользоваться shared_ptr — почему бы и нет.
Еще раз, я не спрашиваю «зачем вообще писать на C++?» (более того, сейчас я пытаюсь убедить коллег перейти с C# на плюсы), я спрашиваю автора «зачем выбирать С++, когда вы уже пишете проект на другом языке, при этом вы делаете это для мест, которые занимают 1% от общего времени исполнения, при этом вы отказываетесь от сырых указателей и всю работу с умными обернули в классы( тем самым лишили себя львиной доли оптимизаций и возможностей), перевели все классы API на shared_ptr (и убили возможность детерминированного управления ресурсами)?»

>>Да и не обязательно вообще начинать с сырых указателей. C++ наиболее логичен и производителен в тех случаях, когда мы используем value-семантику, которой буквально пропитан дизайн языка.
Да, да, да и еще раз да. В C# меня просто бесит, что я не могу передать по значению или хотя бы по константной ссылке. Невозможность отследить эти моменты заставляет писать корявый и медленный код
Я пишу на C++, всегда на нём писал, не считая десяток других языков (включая C#) которыми я пользовался для решения как правило локальных специфических задач (типа поправить скрипты сборки на Perl или обернуть сборку C++ через MC++ для C#/VB.NET). Проекты мы всегда писали на C++, но некоторое время назад понадобилась скриптовая обвязка. Всё отлично работает в C++ без shared_ptr, да и с ним всё замечательно работает. C++ тем и хорош, что нет никаких догм разработки, периодически выходит очередная идиотская книга, где всех учат как «правильно» программирость на C++, после чего по компании проносится очередная эпидемия каких-то немыслимых private-virtual методов или фабрик, которые создают фабрики. В целом C++ нас вполне устраивал, но некоторые вещи лучше заскриптовать, проверить и иметь возможность их быстро править. Что-то совсем прикладное. Здесь нам помог Python и связка Boost.Python. Не везде требуется сетевое взаимодействие, там где оно не нужно, преимущества реактивного исполенения кода написанного на C++ ни с чем не сравнимо.
Ссылки, особенно константные — это здорово, как и размещение на стеке, управление памятью через размещающий new. В общем C++ особо и не выбирали, мы на нём просто без вариантов пишем. Python просто для прикладников иногда очень удобен.
Вот теперь понятно, ситуация ровно наоборот, кою я видел после прочтения. Может это стоит отразить в статье?
Ну первый абзац начинается с того, что мы пишем Платформу на C++ для использования его в прикладных проектах на C++ и Python.
Не работаю я в GameDev (к сожалению, в Ярославле серьёзная разработка GameDev отсутствует).
Указатели в API когда торчат наружу — это всегда плохо. Указатель по определению может быть nullptr, а значит мы заставляем программиста проверять результат, чего он разумеется может и не сделать.
Просто shared_ptr удобен для примера, на котором я объяснял суть copy-on-write подхода. В принципе для однотипных или однородных объектов есть возможность например использовать placement new используя память объекта-владельца (например запись выборки и поля этой записи).
Не работаю я в GameDev

Извините, виноват, gamedev у вас указан интересах. Поторопился и сделал неправильные выводы.
Я когда пишу на C++ обычно пишу интерфейс класса в header, и наследуюсь от этого описания в файле реализации (ну и +статический метод create который создает соответствующий объект). Получается немного сложнее но при этом более логично (для меня) и так-же безопасно. Тут конечно могут возникнуть проблемы с производительностью из-за виртуальных методов, но по факту это очень редко является узким местом. Ну и в реализуемые методы становятся чище. Конструкция типа children_.do_something() для меня куда более интуитивно понятна чем data_.children_.do_something()
Да вы пытаетесь бороться с С++ и сделать из него ваш питон. Ничего хорошего при этом не выйдет. Все ваши решения — это ужасное переусложение, вызванное отчасти и тем, что вы не хотите использовать подход С++.

Например, вы недовольны тем как работает копирование, на самом же деле все несколько проще — если вы хотите копировать — то копируйте. А если не хотите копировать, то сохраняйте ссылки/указатели. И боже упаси вас когда-нибудь использовать COW.
Да ну что вы, я очень люблю C++ и замечательно умею его готовить. «Ужасное переусложнение» никто не увидит, если не заглянет в код реализации. Что до Copy-on-write, то этот подход в том же g++ применяется для std::string(!) что неплохо оптимизирует всевозможную работу с текстом. Почти везде CoW используется в Qt, тот же QString не копирует потроха с текстом при копировании объекта. По-вашему у Qt плохая архитектура?
Ваше переусложение пренепременно сломается сразу после того, как вы уйдете из проекта и какому-то несчастному-таки придется копаться в реализации.

Очень сомневаюсь, что g++ использует COW для std::string, потому что это было бы существленным отступлением от стандарта. Подробнее здесь.

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

Тем не менее реализация GCC std::string сделана на основе copy-on-write, за что мы и любим GCC, можно по сорцам увидеть ref_count. Насчёт стандарта Вы правы, всё верно, не должно быть CoW в std::string, тем не менее данные шарятся между несколькими строками. Завязываться на это нельзя разумеется, потому что например реализация STL от Microsoft сделана по стандарту с честным копированием. Для любителей гарантированного CoW с возможностью декодирования и прочими плюшками есть QString. Если честно, решительно не понимаю за что Вы так не взлюбили copy-on-write подход.
Да вам уже написали ниже. Суть в том, что вместо того, чтобы один раз скопировать, вам приходится все время проверять, не нужно ли это сделать сейчас. В многопоточном приложении это означает доступ к атомарной переменной, со всеми вытекающими последствиями вроде синхронизации памяти.

К тому же, если делать COW самому, то вы, скорее всего, накосячите.
Реализация в gcc сделана еще в те времена, когда это соответствовало стандарту (C++03). А теперь они не могут её изменить, потому что тогда поломается их ABI (это, кстати, к вопросу о стабильном C++ ABI...). Тот же Clang, например, поменял.
На самом деле до С++11 COW было отчасти необходимо, потому что не было механизма move. Теперь же вы можете использовать move, если хотите передать владение, ссылки и указатели, если хотите совместного владения, и копирование, если вы действительно хотите копию.
Ну вообще-то CoW для строк общепризнан плохим решением. Одна из главных причин — плохая производительнось во многопоточной среде (потому что счётчики ссылок должны быть синхронизированы). Кстати, в новом стандарте C++11 CoW для строк больше не легален.
Ну статья от 99-го года, а CoW жив и по сей день. Массовое копирование при многопоточном доступе также будет причиной плохой производительности. Безопаснее вернуть std::string вместо ссылки на него, опять же как это сказано в статье, это освобождает от обязательств в реализации и завязки на хранение именно std::string. Если нет CoW, то при копировании из поля класса в значение результата будет довольно дорогое копирование, сложность которого невозможно померять, так как строки произвольной длины. Есть всевозможные оптимизации строк, именно по причине дорогого копирования, но как раз в данном случае мы получим проседание по производительности при честном копировании строк.
Ну статья от 99-го года, а CoW жив и по сей день.
CoW жив и по сей день потому что совместимость. C++11-совместимая версия была добавлена в GCC восемь лет назад, но стандартизация несколько затянулась и потому «большой слом» всё ещё не случился.

Есть всевозможные оптимизации строк, именно по причине дорогого копирования, но как раз в данном случае мы получим проседание по производительности при честном копировании строк.
Прогоните ваши тесты, очень может быть, что и в вашем случае (как и у нас) CoW версия будет медленнее. Часто — заметно медленнее.
(потому что счётчики ссылок должны быть синхронизированы).

Эээ — atomic?
Вобще с 99го года очень много воды утекло.
Ну атомики тоже очень медленны. (И одновременно очень быстры по сравнению с мьютексами)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории