вторник, октября 06, 2009

Google C++ Style Guide

Google C++ Style Guide как-то упомянули у меня в комментариях и я решила продублировать ссылку на него здесь. Это стандарт кодирования гугловых open source С++ проектов. Этот Style guide содержит некоторые спорные решения как то: отказ от исключений, отказ от RTTI.

В свое время Google C++ Style Guide активно обсуждался на Habrahabr'е.

42 комментария:

  1. А у нас на работе всё та же венгерская нотация... как я её недолюбливаю...

    ОтветитьУдалить
  2. А я вот думаю, что Кодинг Стайл Гайд - это типа соглашение между программистами о том как нужно писать.

    Только чем больше программистов учавствуют в этом соглашении - тем больше размер гайда.

    Мыслю, что было бы правильно в каждой команде иметь свой минимал гайд... Соблюдать все правила из толстого-претолстого документа крайне сложно... кодинг стайл должен влезать на одну страницу, чтобы каждый мог повесить его на стенку в рамочке. :D

    У нас тоже пока никакого нету, хотя венгерскую нотацию мы не используем, слава богу.

    ОтветитьУдалить
  3. 2Андрей Валяев:
    Мыслю, что было бы правильно в каждой команде иметь свой минимал гайд...

    Дык так и есть. Почти везде где я работала были внутренние стандарты кодирования. У вас не так?

    ОтветитьУдалить
  4. У нас есть негласные правила... Мы еще только учимся.. :) Код ревьюв вот начали делать... Может скоро и документик какой сформулируем...

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

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

    ОтветитьУдалить
  5. 2Андрей Валяев:

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


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

    ОтветитьУдалить
  6. Венгерскяа нотация - какой-то отстой. Она была хороша, когда нужно было смотреть на код и представлять тип переменной. Зачем это в эпоху IDE?

    ОтветитьУдалить
  7. 2Денис Радченко: Ну изначально префиксы обозначали вовсе не это.

    Джоэл про это писал...

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

    Типа того: xPos = yPos - сразу видно что ерунда... :)

    Но потом кто-то придумал кодировать тип, и пошло - поехало.

    ОтветитьУдалить
  8. 2 Андрей Валяев:
    "глобальную категорию переменной" - не совсем точно... Скорее "суть, назначение", чтобы нельзя было

    currentCell = currentRow;
    даже если обе переменные типа int.
    А префиксы определяющие область видимости были в оригинальном документе.

    2Денис Радченко:
    А не все сейчас IDE и используют... Мне, например, простую идею проще быстро набросать в блокноте, а потом вставить в Студию, ибо перегружена оная кучей фич.
    И такой вопрос: коде ревью Вы тоже из IDE делаете?

    ОтветитьУдалить
  9. Вполне себе разумный coding standard. Особенно соглашусь с фразой "BE CONSISTENT".

    Из спорных моментов отмечу неиспользование исключений, но и здесь мотивация ясна. К тому же: "Things would probably be different if we had to do it all over again from scratch." С остальным примириться совсем легко.

    ОтветитьУдалить
  10. А меня еще раздражают отступы пробелами... по моему, на порядок проще один раз нажать на таб, чем выравнивать код пробелами (хотя для IDE и продвинутых редакторов это и не актуально)... У них какой-то микс K&R и GNU стайла...

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

    Лично я кодревьюв делаю в kdiff3, который вызывается непосредственно из перфорса... ревьювить постоянно все - бессмысленно, надо смотреть на то, что изменилось.

    ОтветитьУдалить
  11. Анонимный8/10/09 00:57

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

    ОтветитьУдалить
  12. Спасибо за ссылку, добавлю в свой ToRead-список.
    В свое время для меня было в какой-то мере откровением прочесть "Joint Strike Fighter Air Vehicle, C++ Coding Standards, December 2005". Стандарт кодирования на С++, принятый в Lockheed Martin Corporation при разработке многоцелевого ударного истребителя. Не менее серьезная контора и серьезный проект.
    Так вот там тоже попадались странные на первый взгляд правила, вроде отказа от исключений и рекурсий.
    Кстати, по поводу исключений Спольски, если я не ошибаюсь, написал в той же самой статье, в которой он защищал Венгерскую нотацию (в оригинальной ее трактовке).

    ОтветитьУдалить
  13. Анонимный8/10/09 02:26

    Отказ от исключений предлагаю продолжить: предлагаю правило: отказ от множественных return в функции (гыгы).

    ОтветитьУдалить
  14. Можно все запретить...

    У меня все будут счастливы, а кто не будет - того я... (с) Бармалей

    Только каждую возможность придумывали не просто так... у auto_ptr тоже есть свой смысл. На мой взгляд исключения сильно упрощают код. Конечно этот механизм тяжелый, и это надо понимать. И конечно необдуманным испрользованием чего угодно можно здорово испохабить код.. Дык это можно сделать и несмотря на все запреты. :)

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

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

    ЗЫ: На испытательный срок - один кодинг гайд, 5 лет отработал - другой. Через 10 лет программирования тебя отводят в секретную комнату, где лежит совершенно секретный кодинг гайд, в котором написано что нет никаких гайдов... :D

    ОтветитьУдалить
  15. На то он и C++, чтобы был выбор и широкие возможности. За что я его и люблю. И именно из-за своих больших возможностей C++, как никакой другой язык, пожалуй, нуждается в подобного рода сборниках правил - они как база, от которой следует по умолчанию отталкиваться при проектировании или написании программ.
    И, как и всегда, из всяких правил есть исключения (pun intended). Но чтобы их допускать, нужны веские причины и достаточные полномочия.

    ОтветитьУдалить
  16. 2Андрей Валяев:
    А меня еще раздражают отступы пробелами... по моему, на порядок проще один раз нажать на таб, чем выравнивать код пробелами (хотя для IDE и продвинутых редакторов это и не актуально)...
    Насчет табов и пробелов, нажимать много раз пробел не нужно - многие IDE позволяют настроить замену таба определенным количеством пробелов
    плюс этого подхода проявляется когда открываеш код в каком то нестандартном вьювере и в нем скажем увтановленно значение таба вдвое больше чем в твоей родной IDE весь код магическим образом оказывается где то глубоко за правым краем экрана, а если табы заменены пробелами, то такого эффекта нет, форматирование кода остается одинаковым в любом вьювере

    Ну а насчет того, какая нотация лучше, так это тема для отдельно вынесенного холовара где нибудь на просторах rsdn

    ОтветитьУдалить
  17. Анонимный8/10/09 23:16

    Андрей Валяев: да-да :-) а кто не shared ptr, тот в Stl огребает море overhead'ов. И потом вопросы задает не "как сделать, чтобы не копировать сложные объекты в список", а "что вы возьмете, vector или list"? :-) А кто не auto ptr и не handle wrapper- зачем им С++ вообще?

    ОтветитьУдалить
  18. Анонимный9/10/09 01:20

    Написание ПО для истребителей с использованием исключений, RTTI... Вы лулзность этой фразы не понимаете?)

    >> Но скорость имеет значение далеко не всегда.
    if (!привет мир) throw упс!
    да)

    з.ы. я не троль :)

    ОтветитьУдалить
  19. 99,9% ПО разрабатывается не для истребителей!

    Да и Google вроде не занимается такой ерундой, как истребители.

    И чтобы вот так вот, из за истребителей, запрещать RTTI и исключения?

    :D

    ОтветитьУдалить
  20. Не хочу разводить холивар, но:
    1) как и написано у Страуструпа - исключения должны оставаться исключениями, т.е. "бросать" их надо в исключительных ситуациях (т.е. в тех ситуациях, которые не являются обычными, предусмотренными логикой программы ветвлениями)
    2) без RTTI (сам лично не использую вообще) и исключений это уже не C++, так как даже new "бросает" исключение (и это правильно, что бы там не писали в Google и Mozilla)
    3) насчет JSF Coding Standard - там все под embedded заточено и в этом плане все разумно написано, но есть пример того как "подружить" "return error code" и "exception" - см. как используется boost::system::error_code в boost::asio (этот пример хорош еще и тем, что там как раз не "return" используется для возврата "error code")
    4) боязнь использовать что-то, потому что не на всех платформах компиляторы позволяют "это" (правильно) собрать (и в быстрый код) вполне разумна, но способствует увеличению парка недо-С++ компиляторов и сроку их жизни
    5) надо трезво понимать, что серьезный код (больших объемов) так или иначе не сделать кроссплатформенным "от и до" (спросите у разработчиков Java-игр для мобильных телефонов) - в той или иной мере все равно придется проводить портирование - вопрос только в его объеме
    6) исходя из [5] - не понятно, почему Google так боится исключений - единственное что приходит в голову - они просто не желают тратить время на обсуждение того, где можно "кидать exception", а где нельзя
    7) ну и как вывод - без исключений не обойтись (если, конечно, не вывернуть и не "переизобрести" C++) и в любом случае их будут поддерживать все платформы (Windows CE и все, что на ней есть уже поддерживает) - вопрос только в том, всегда ли мы вправе "бросать" исключение - тут просто еще раз проштудировать Бьярна - у него пожалуй, самый взвешенный подход ко всему, что есть в C++

    P.S. а мне все таки нравится JSF Coding Standard, но naming conventions больше импонируют STL и Boost C++ Libraries, хотя в если в команде принято другое (хорошо если еще JSF-подобное, а не Camel) то профессионализм :) трубет строго подчиняться.

    ОтветитьУдалить
  21. 2Marat:
    6) исходя из [5] - не понятно, почему Google так боится исключений - единственное что приходит в голову - они просто не желают тратить время на обсуждение того, где можно "кидать exception", а где нельзя

    За Google не скажу, но везде, где я работала, исключения были либо запрещены стандартами кодирования, либо не рекомендовались к использованию. Причины: исключения повышают сложность кода и ухудшают читаемость.
    AFAIK, исключения не используются в российском геймдеве в принципе. Не знаю как там в забугорье дела обстоят.

    ОтветитьУдалить
  22. В этом стандарте меня лчно напрягло то, что запрещается использовать директивы using. В сочетании с boost'овскими (да и просто std) библиотеками, где любят на каждый чих создавать namespace, код быстро замусорится уточнениями контекста.

    boost::gil::opencv::resize(boost::gil::const_view(my_src), boost::gil::view(my_dst) );

    (Пример навскидку)

    ОтветитьУдалить
  23. 2Алёна:
    1) Я не "бью в грудь" - юзайте исключения, но пытаюсь донести, что надо обязательно учитывать возможность появления исключений (хотя бы потому, что пишем на языке, в который они буквальныо "зашиты") - Java-way в плане исключений безобразен
    2) предпочитаю
    а) не бросать исключение, если можно обойтись без него,
    б) гарантии Абрахамса, точнее стараюсь придерживаться только гарантии "неутечки ресурсов"
    в) стараюсь не обрабатывать исключения (т.к. это по большей части ошибка) - просто даю программе "упасть" - тут я полностью согласен со Страуструпом
    3) с практической стороны вызывает интерес то, как например, лично Вы поступаете с "new" (вот только не надо переписывать мнение разработчиков Qt или Mozilla - уже много раз читал, поэтому хватит просто ссылки)
    4) Если использовать STL (или просто C++ Standard Library), то опять же без исключений "не получится" - понимаю, что в gamedev-е на С++ почти никто не использует STL (и C++ Standard Library? для чего же тогда они нужны? :)
    5) использовать RAII и везде "защищаться" от исключений действительно не просто (посмотреть хотя бы в исходники boost::asio) - но исключения не так уж и запутывают код - сравните со множеством if-else (ACE vs. boost::asio).
    6) "исключения для исключений" - если можно, то я стараюсь использовать boost::system::error_code (как в boost::asio - передавая его через ссылку), но есть и места, где без них не обойтись - конструктор, operator [] и т.д. любой знает эти примеры
    7) насколько я вижу (и где-то читал - Вы наверняка тоже) - не такая уж и малая часть кода является no-throw.
    9) все порываюсь изучить Intel TBB - неужели и там нет исключений? (я имею ввиду "совсем нет") - а это точно используется в "забугорном геймдеве" - недавно что-то там анонсировали в UE3
    9) ...все таки холивар... так что простите... просто интересно мнение - все таки "вариться в собственном соку" еще никому не пошло на пользу.

    P.S. Я далеко не профи в C++, хотя и хотел бы. Да и в production на C++ почти ничего не писал, так что очень ценю мнение опытных разработчиков и стараюсь не навязывать свое.

    ОтветитьУдалить
  24. 2Marat:
    1) Я не "бью в грудь" - юзайте исключения

    Да я в общем тоже без всякого подтекста говорю. Просто вот так дела обстоят.

    3) с практической стороны вызывает интерес то, как например, лично Вы поступаете с "new"

    У нас свой менеджер памяти, оператор new переопределен. При каких-либо проблемах с памятью выдаем assert, который есть только в дебаге и который вырезается в релизе.

    9) все порываюсь изучить Intel TBB - неужели и там нет исключений? (я имею ввиду "совсем нет") - а это точно используется в "забугорном геймдеве" - недавно что-то там анонсировали в UE3

    Про Intel TBB я совсем не в курсе.

    9) ...все таки холивар... так что простите...

    Да не, по-моему всё довольно конструктивно.

    ОтветитьУдалить
  25. 2Алёна
    С менеджером памяти - ожидаемо, т.е. многие так и делают (Mozilla). А чем тогда этот вариант отличается от "обычного new" - в release нет исключений? значит, все таки, возвращаемое значение всегда проверяется на NULL (пока все же правильнее говорить "0")? Если нет, то, значит, просто принимается, что если при тестах не было "out of memory", то и у пользователя тоже (никогда) не будет? Согласен, с тем, что это допустимо. Но "new" не единственное место - см. operator= и т.д. Конечно, операторы перегружать - последнее дело, но иногда все-таки нужно (как раз читабельность кода повышается). Я еще раз повторюсь, что прежде чем "бросить exception" надо "100 (10000...00) раз подумать" - аналогично и для try-catch. Вот в связи с этим, мне кажется, Google просто "боится" терять время на "священные войны". Хотя игнорирование RAII еще никому на пользу не пошло. Да и "хороший C++ программист" не должен злоупотреблять (именно злоупотреблять) всем тем, что есть в C++ (неужели Google "своих" такими не считает?). Кстати, как Вам "Herb Sutter, Andrei Alexandrescu - C++ Coding Standards"? Это конечно не Google (и уж тем более не JSF), но лично мне кажется хорошей отправной точкой для построения своего (т.е. для полного включения).

    ОтветитьУдалить
  26. 2Marat:
    С менеджером памяти - ожидаемо, т.е. многие так и делают (Mozilla). А чем тогда этот вариант отличается от "обычного new" - в release нет исключений?

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

    ОтветитьУдалить
  27. 2Алёна
    Раз уж отошел от темы, то продолжу...

    Что касается менеджера памяти:
    1) интересно Ваше мнение по поводу http://www.hoard.org/
    2) -//-//- по поводу memory allocator в Intel TBB (task memory allocator)
    3) -//-//- custom memory allocation в boost::asio - конечно там из-за этого сильно осложняется (именно осложняется - как-нибудь соберусь и напишу про ее "тупую" реализацию) раздельная компиляция, но в "effective net code" это уже должно отходить на второй план.

    Если что-то из этого не знаете (важнее "не просто много знать", а уметь эффективно использовать то, что знаешь - не так ли?), то прошу как-нибудь выкроить время и посмотреть - все же многие читают этот блог (из моих знакомых) и всем интересно Ваше мнение.

    P.S. Это последний "пост" - больше "сорить" не к чему.

    ОтветитьУдалить
  28. Анонимный9/10/09 16:04

    >> Причины: исключения повышают сложность кода и ухудшают читаемость.
    я не верю всяким байкам о трудно читаемости при использовании goto, throw (и что там у вас ещё). скорость решает всйо. вротмненоги если в cryengine expressive c++! ибо в gamedev главное растрата тиков. не так ли? :))

    ОтветитьУдалить
  29. Анонимный9/10/09 16:16

    >> И чтобы вот так вот, из за истребителей, запрещать RTTI и исключения?
    нет конечно :) более того я скажу что использую исключения (при инициализации ядра конструктор кидает если продолжать невозможно). но повсеместно использовать это глупо. давайте ещё virtual'ить все :))

    ОтветитьУдалить
  30. 2Анонимный:

    я не верю всяким байкам о трудно читаемости при использовании goto, throw (и что там у вас ещё).

    Не надо верить, правильно. Я проверяла goto на своей шкуре. Не понравилось.

    в gamedev главное растрата тиков. не так ли? :))

    Нет. Очень быстрая и очень глюкавая игра никому не нужна.

    ОтветитьУдалить
  31. Анонимный9/10/09 20:11

    Очень быстрая и очень глюкавая игра никому не нужна
    .net? :)

    з.ы. вот почему cryengine и unreal делаются годами
    з.з.ы. всё молчу :))

    ОтветитьУдалить
  32. Анонимный11/10/09 15:02

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

    Три года ранее. Компания Quest. Сервисы. Во всех случаях кидается исключение class Exc { strText, hresultCode } или его наследники- как писать по-другому тесты- не знаю. Сейчас использую в своей практике. Любой try заканчивается catch (Exc).

    Собственно, точно так же, как в java писали try .. catch (throwable). Потому что проблемы натива нам пофик и были нужны исключительно для лога. И обязательно везде finally... иначе ресурсы ни фига не освободятся.

    Думаю, WinAPI, тот же CreateFile внутри себя может легко кидать Exceptions. Наружу он дает код ошибки. По этому коду ошибки exception кидает уже мой Wrapper.

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

    Порадовали китайцы. В огромной системе из сотни проектов был только один, в котором разрешили exception'ы. Я специально переписывался с этим инженером, чтобы узнать, почему у него было просветление- ну, что он там использовал- медитацию, цигун... Оказалось, что он их потом запретил. "Потому что код уменьшается на N килобайтов".

    Китайцы очень жестко следовали Qt: НИ ОДНОГО исключения. Только в коде не было анализа НИ ОДНОГО кода возврата. Для них НУЖНО писать throw "Wrong filename RTFM" :-).

    P.S.: Автоматические тесты рулят.

    ОтветитьУдалить
  33. Дмитрий12/10/09 00:38

    Как по мне, то самое верное и важное утверждение по поводу стандартов кодирования: "Не столь важно, какие именно у вас стандарты кодирования, важно, что они есть, и все им следуют". Google C++ Style Guide - более чем достойный кандидат для основы стандартов кодирования и для размышлений в принципе. Особенно понравилось то, что для большинства решений есть описание, за, против и вывод.

    ОтветитьУдалить
  34. Чесно говоря, очень странно читать после стольких лет существования исключений в мейнстримовых языках программирования про использование и неиспользование исключений. Тем более, что в ряде языков (Ruby, Python, Java, C#) -- исключения вообще являются единственной формой извещения об ошибках.

    Уж не знаю, насколько сильно исключения в современных C++ных компиляторах сказываются на скорости работы. Но вот на надежность и отказоустойчивость программ исключения влияют гораздо лучше, чем коды возврата или глобальный errno.

    ОтветитьУдалить
  35. Анонимный13/10/09 09:22

    2Алёна:
    >AFAIK, исключения не используются
    >в российском геймдеве в принципе.

    Почему же, есть компании, где используются вовсю. :)

    >При каких-либо проблемах с памятью
    >выдаем assert, который есть только
    >в дебаге и который вырезается в
    >релизе.

    То есть, у пользователя при нехватке памяти игра просто упадёт?

    ОтветитьУдалить
  36. 2dtjurev:
    Почему же, есть компании, где используются вовсю. :)

    :-)

    То есть, у пользователя при нехватке памяти игра просто упадёт?

    У нас консоль, а не PC. Протестировали на одной - получаем такое же поведение на всех. Нет такого, что у тестеров памяти хватало, а у пользователя вдруг не хватает.

    Плюс у нас суровый defensive программинг. Значения проверяются, но перед обращением.

    ОтветитьУдалить
  37. Анонимный13/10/09 20:15

    Считаю, что эта тема для флудинга. не более того. в школе учат буковки аккуратно писать и что? все пишут как нравится. и это хорошо.

    ОтветитьУдалить
  38. Считаю, что эта тема для флудинга. не более того.

    Достаточно вспомнить, что уставы и инструкции по технике безопасности написаны кровью. Можно до многих вещей доходить набивая шишки на собственном лбу, а можно и переняв чужой опыт. Рекомендации Google выгодно оличаются от многих других тем, что обосновывают каждое из ограничений. Это выжимка из чужого опыта. Его можно обсуждать и отвергать, но ознакомиться все равно полезно.

    ОтветитьУдалить
  39. Анонимный29/11/09 22:48

    Где-то прочитал отличный вывод на тему всех таких попыток "оптимизировать" codestyle C++ (запрещая использование чего-то - как исключений в случае Google):

    "Чтобы эффективно писать на C++, придется использовать ВСЕ возможности языка."

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

    ОтветитьУдалить
  40. Анонимный29/11/09 22:56

    > Китайцы очень жестко следовали Qt: НИ ОДНОГО исключения. Только в коде не было анализа НИ ОДНОГО кода возврата.

    Если очень жестко следовать Qt, то получится чудовищный код, в котором на каждый чих (i.е. в начале каждой функции, даже если она вида A+=B; ) делается до десяти проверок: а если у нас не нулевой указатель на параметры; а выглядит ли размер данных таким, как нужно; а доступны ли нам некие глобальные объекты (типа общего менеджера объектов - или он по каким-то причинам умер), а знает ли этот менеджер (который может быть удаленным сетевым сервером!) про объект, у которого мы собираемся что-то спросить...

    Т.о. код раздувается в среднем в пять раз.

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

    ОтветитьУдалить
  41. Благодарю за интересную статью и комментировавших - за дискуссию. Только смените пожалуйста ссылку в посте - сейчас обсуждение на Хабре находится на http://habrahabr.ru/links/38329/
    Причём прямой поиск результатов не даёт ( http://habrahabr.ru/search/?q=Google+C%2B%2B+Style+Guide ), пришлось походить "путями нормальных героев".

    ОтветитьУдалить
  42. hydrevt

    Благодарю за интересную статью и комментировавших - за дискуссию. Только смените пожалуйста ссылку в посте

    поправила, спасибо

    ОтветитьУдалить