Должны ли быть утверждения в сборках релиза


20

Поведение по умолчанию assert в C ++ - ничего не делать в сборках релиза. Я предполагаю, что это сделано из соображений производительности и, возможно, чтобы пользователи не видели неприятных сообщений об ошибках.

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

Кроме того, аргумент производительности для меня имеет значение только тогда, когда это измеримая проблема. Большинство assertс в моем коде не намного сложнее, чем

assert(ptr != nullptr);

который будет иметь небольшое влияние на большую часть кода.

Это приводит меня к вопросу: должны ли утверждения (означающие концепцию, а не конкретную реализацию) быть активными в сборках релиза? Почему нет)?

Обратите внимание, что этот вопрос не о том, как включить утверждения в сборках релиза (например, #undef _NDEBUGили использовать реализацию самоопределения). Кроме того, речь идет не о включении утверждений в коде сторонней / стандартной библиотеки, а в коде, контролируемом мной.


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

2
Есть старая поговорка, которая гласит: «Удаление утверждений - это что-то вроде ношения спасательного жилета для тренировки в гавани, но затем оставление спасательных жилетов позади, когда ваш корабль уходит в открытый океан» ( wiki.c2.com/?AssertionsAsDefensiveProgramming ) , Я лично держу их включенными в сборках по умолчанию. К сожалению, эта практика не очень распространена в мире C ++, но, по крайней мере, известный ветеран C ++ Джеймс Канзе всегда спорил в пользу сохранения утверждений в сборках релизов: stackoverflow.com/a/12162195/3313064
Кристиан Хакл,

Ответы:


20

Классика assert- это инструмент из старой стандартной библиотеки C, а не из C ++. Это все еще доступно в C ++, по крайней мере, по причинам обратной совместимости.

У меня нет точной временной шкалы стандартных библиотек C под рукой, но я уверен, что она assertбыла доступна вскоре после того, как K & R C появилась вживую (около 1978 года). В классическом C для написания надежных программ добавление тестов NULL-указателей и проверка границ массивов необходимо выполнять гораздо чаще, чем в C ++. Можно избежать грубых тестов указателей NULL, используя ссылки и / или умные указатели вместо указателей, а также используяstd::vector проверка границ массивов часто не требуется. Более того, хит производительности в 1980 году был определенно гораздо важнее, чем сегодня. Поэтому я думаю, что именно по этой причине «assert» был разработан, чтобы быть активным только в отладочных сборках по умолчанию.

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

  • проверяет некоторое условие (и останавливает выполнение в области, где условие не выполняется)

  • выдает четкое сообщение об ошибке, если условие не выполняется

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

  • позволяет внешней области действия для каждого конкретного случая решать, должна ли программа завершиться изящно или должна продолжаться.

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

Поскольку классика assertне предоставляет эти функции, она не подходит для сборки релиза, если предположить, что сборка релиза - это то, что каждый развертывает в производство.

Теперь вы можете спросить, почему в стандартной библиотеке C нет такой функции или механизма, который обеспечивает такую ​​гибкость. На самом деле, в C ++ есть стандартный механизм, который имеет все эти функции (и даже больше), и вы это знаете: это называется исключениями .

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


Ах, совсем забыл о наследии C (после всего его #include <cassert>). Это полностью имеет смысл сейчас.
Никто

1
Я полностью не согласен со всем этим ответом. Возникновение исключения, когда ваше приложение обнаружило, что его исходный код содержит ошибки, является последним средством в таких языках, как Java, которые не могут работать лучше, но в C ++ непременно завершают программу немедленно в такой ситуации. Вы не хотите, чтобы разматывание стека приводило к выполнению каких-либо дальнейших операций в этой точке. Неисправная программа может записывать поврежденные данные в файлы, базы данных или сетевые сокеты. Просто завершите работу как можно скорее (и, возможно, перезапустите процесс с помощью внешнего механизма).
Кристиан Хакл

1
@ChristianHackl: Я согласен, что бывают ситуации, когда прекращение работы программы является единственно возможным вариантом, но тогда это должно произойти и в режиме выпуска, и это также assertявляется неправильным инструментом (исключение может действительно быть и неправильной реакцией). ). Тем не менее, я уверен, что на самом деле assertтакже использовался для многих тестов в C, где в C ++ сегодня будут использоваться исключения.
Док Браун

15

Если вы обнаружите, что хотите, чтобы утверждения были включены в выпуске, вы попросили, чтобы утверждения выполняли неправильную работу.

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

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

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

Я говорю не о конкретной реализации утверждения, а о концепции утверждения. Я не хочу злоупотреблять утверждением или путать читателей. Я хотел спросить, почему это так во-первых. Почему нет дополнительного release_assert? В этом нет необходимости? В чем причина того, что assert отключен в релизе? - никто

Почему нет relayse_assert? Честно говоря, потому что утверждения не достаточно хороши для производства. Да, есть необходимость, но ничто не удовлетворяет эту потребность хорошо. О, конечно, вы можете создать свой собственный. Механически вашей функции throwIf просто нужно использовать bool и исключение. И это может соответствовать вашим потребностям. Но вы действительно ограничиваете дизайн. Вот почему меня не удивляет то, что в вашей языковой библиотеке нет системы, генерирующей исключения типа assert. Конечно, это не значит, что вы не можете этого сделать. Другие имеют . Но для случая, когда что-то идет не так, для большинства программ приходится 80% работы. И до сих пор никто не показал нам хороший размер, подходящий для всех решений. Эффективно справиться с этими случаями может стать сложным, Если бы у нас была консервированная система release_assert, которая не соответствовала нашим потребностям, я думаю, что она принесла бы больше вреда, чем пользы. Вы просите хорошую абстракцию, которая бы означала, что вам не придется думать об этой проблеме. Я тоже хочу один, но, похоже, мы еще там.

Почему утверждения отключены в выпуске? Утверждения были созданы в разгар возраста кода лесов. Код, который мы должны были удалить, потому что знал, что мы не хотим, чтобы он был в производстве, но знал, что мы хотим запустить в разработке, чтобы помочь нам находить ошибки. Утверждения были более чистой альтернативой if (DEBUG)шаблону, который позволял нам оставлять код, но отключать его. Это было до того, как модульное тестирование стало основным способом отделения кода тестирования от производственного кода. Утверждения по-прежнему используются сегодня даже опытными юнит-тестерами, чтобы прояснить ожидания и покрыть случаи, когда они все же лучше, чем юнит-тестирование.

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


2
Я предполагаю, что мой вопрос: почему этот код должен быть удален перед выпуском? Проверки не сильно снижают производительность, и если они терпят неудачу, есть определенно проблема, о которой я бы предпочел более прямое сообщение об ошибке. Какое преимущество дает мне исключение вместо утверждения? Я, вероятно, не хотел бы, чтобы несвязанный код был в состоянии catch(...)это сделать.
никто

2
@Nobody Самым большим преимуществом отказа от использования утверждений для нужд без утверждений не является смущение ваших читателей. Если вы не хотите, чтобы код был отключен, не используйте идиомы, которые сигнализируют о том, что он будет. Это так же плохо, как использовать if (DEBUG)для управления чем-то другим, кроме отладки кода. Микро оптимизация может ничего не значить в вашем случае. Но идиома не должна быть подорвана только потому, что она вам не нужна.
candied_orange

Я думаю, что я не достаточно ясно выразил свои намерения. Я говорю не о конкретной реализации, assertа о концепции утверждения. Я не хочу злоупотреблять assertили путать читателей. Я хотел спросить, почему это так во-первых. Почему нет дополнительного release_assert? В этом нет необходимости? В чем причина того, что assert отключен в релизе?
Никто

2
You're asking for a good abstraction.Я не уверен в этом. Я в основном хочу иметь дело с проблемами, когда нет восстановления, и это никогда не должно происходить. Возьмите это вместе с, Because production code needs to [...] not format the hard drive [...]и я бы сказал, что для случаев, когда инварианты нарушены, я бы взял утверждение в выпуске через UB в любое время.
Никто

1
@ Никто, "проблемы, когда нет восстановления", может быть решен, бросая исключения и не перехватывая их. Вы можете заметить, что они никогда не должны встречаться в тексте сообщения об исключении.
candied_orange

4

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


4

assertэто форма документации, как комментарии. Как и комментарии, вы обычно не отправляете их клиентам - они не входят в код выпуска.

Но проблема с комментариями состоит в том, что они могут устареть, и все же они остаются. Поэтому утверждения хороши - они проверяются в режиме отладки. Когда утверждение устареет, вы быстро его обнаружите и все равно будете знать, как исправить утверждение. Тот комментарий, который устарел 3 года назад? Чье-то предположение.


1
Таким образом, отмена неудачного инварианта до того, как что-то произойдет, не является ли допустимым вариантом использования?
Никому

3

Если вы не хотите, чтобы «утверждение» было отключено, напишите простую функцию, которая имеет аналогичный эффект:

void fail_if(bool b) {if(!b) std::abort();}

То есть assertдля испытаний, когда вы хотите, чтобы они ушли в поставляемом продукте. Если вы хотите, чтобы этот тест был частью определенного поведения программы, assertэто неправильный инструмент.


1
Я хорошо это знаю. Вопрос скорее в том, почему по умолчанию это так.
Никто

3

Бессмысленно спорить, что должен делать assert, он делает то, что делает. Если вы хотите что-то другое, напишите свою собственную функцию. Например, у меня есть Assert, который останавливается в отладчике или ничего не делает, у меня есть AssertFatal, который вызывает сбой приложения, у меня есть функции bool Asserted и AssertionFailed, которые утверждают и возвращают результат, так что я могу как утверждать, так и обрабатывать ситуацию.

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


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

Хм, не verify(cond)будет ли нормальный способ утверждать и возвращать результат?
Дедупликатор

1
Я пишу на Ruby, а не на C, но я не согласен с большинством ответов, приведенных выше, поскольку остановка программы с печатью обратной трассировки работает для меня как метод защитного программирования ....... мой комментарий не подходит Максимальный размер комментария в символах, поэтому я написал другой ответ ниже.
Накилон

2

Как указывали другие, assertэто своего рода последний оплот защиты от ошибок программистов, которые никогда не должны происходить. Это проверки работоспособности, которые, мы надеемся, не потерпят неудачу влево и вправо к моменту отправки.

Он также предназначен для исключения из стабильных сборок релиза по любым причинам, которые могут оказаться полезными для разработчиков: эстетика, производительность, все, что они захотят. Это часть того, что отделяет отладочную сборку от сборки выпуска, и по определению сборка выпуска лишена таких утверждений. Таким образом, существует подрыв дизайна, если вы хотите выпустить аналогичную «сборку релиза с утверждениями на месте», которая была бы попыткой сборки релиза с определением _DEBUGпрепроцессора и без NDEBUGопределения; это больше не релизная версия.

Дизайн распространяется даже на стандартную библиотеку. В качестве очень простого примера среди множества других реализаций std::vector::operator[]будет assertпроверка работоспособности, чтобы убедиться, что вы не проверяете вектор вне границ. И стандартная библиотека начнет работать намного, намного хуже, если вы включите такие проверки в сборке релиза. Ориентирvector использованияoperator[]и заполнитель ctor с такими утверждениями, включенными в простой старый динамический массив, будет часто показывать, что динамический массив работает значительно быстрее, пока вы не отключите такие проверки, поэтому они часто влияют на производительность далеко, далеко не тривиальными способами. Проверка нулевого указателя здесь и проверка вне границ могут действительно стать огромными расходами, если такие проверки применяются миллионы раз для каждого кадра в критических циклах, предшествующих коду, так же просто, как разыменование умного указателя или доступ к массиву.

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

И все же, наконец, и в некоторой степени с вами согласился, я мог видеть разумный случай, когда вы могли бы захотеть передать тестерам что-то похожее на отладочную сборку во время альфа-тестирования, например, с небольшой группой альфа-тестеров, которые, скажем, подписали NDA , Там это может упростить альфа-тестирование, если вы дадите своим тестировщикам нечто иное, чем полная сборка релиза, с некоторой отладочной информацией, прикрепленной к некоторым функциям отладки / разработки, таким как тесты, которые они могут выполнить, и более подробный вывод во время работы программного обеспечения. Я, по крайней мере, видел, как некоторые крупные игровые компании делают такие вещи для альфы. Но это для чего-то вроде альфа-тестирования или внутреннего тестирования, когда вы искренне пытаетесь дать тестерам нечто иное, чем сборка релиза. Если вы на самом деле пытаетесь отправить релизную сборку, то по определению_DEBUG определяется, иначе это действительно сбивает с толку разницу между сборкой «отладка» и «выпуск».

Почему этот код должен быть удален перед выпуском? Проверки не сильно снижают производительность, и если они терпят неудачу, есть определенно проблема, о которой я бы предпочел более прямое сообщение об ошибке.

Как указано выше, проверки не обязательно являются тривиальными с точки зрения производительности. Многие из них, вероятно, тривиальны, но опять же, даже стандартная библиотека использует их, и это может повлиять на производительность неприемлемым образом для многих людей во многих случаях, если, скажем, обход произвольного доступа std::vectorзанял в 4 раза больше времени в том, что должно быть оптимизированной сборкой выпуска из-за его проверки границ, которая никогда не должна потерпеть неудачу.

В предыдущей команде нам фактически пришлось сделать так, чтобы наша библиотека матриц и векторов исключала некоторые утверждения в определенных критических путях, просто чтобы ускорить отладочные сборки, потому что эти утверждения замедляли математические операции более чем на порядок до точки, где это было Начинаем требовать от нас подождать 15 минут, прежде чем мы сможем даже найти интересующий нас код. Мои коллеги на самом деле хотели просто удалитьassertsпрямо, потому что они обнаружили, что просто делать это имело колоссальную разницу. Вместо этого мы остановились на том, чтобы заставить критические пути отладки избегать их. Когда мы заставили эти критические пути напрямую использовать векторные / матричные данные, не проходя проверку границ, время, необходимое для выполнения полной операции (которая включала в себя не только векторную / матричную математику), сократилось с минут до секунд. Так что это крайний случай, но определенно утверждения не всегда незначительны с точки зрения производительности, даже близко.

Но также это просто способ asserts, которым разработаны. Если они не оказали такого огромного влияния на производительность по всем направлениям, то я мог бы предпочесть его, если бы они были спроектированы как нечто большее, чем функция отладочной сборки, или мы могли бы использовать, vector::atкоторый включает проверку границ даже в сборках релизов и выбросы за пределы доступ, например (пока с огромным ударом по производительности). Но в настоящее время я нахожу их дизайн намного более полезным, учитывая их огромное влияние на производительность в моих случаях, как функцию только для отладочной сборки, которая не указывается при NDEBUGопределении. По крайней мере, для случаев, с которыми я работал, сборка релиза имеет огромное значение для исключения проверок работоспособности, которые никогда не должны были вообще сбоить.

vector::at против vector::operator[]

Я думаю, что различие этих двух методов лежит в основе этого, а также альтернативы: исключения. vector::operator[]Реализации, как правило, assertгарантируют, что доступ за пределами границ вызовет легко воспроизводимую ошибку при попытке доступа к вектору за пределами границ. Но разработчики библиотеки делают это с предположением, что в оптимизированной сборке релиза это не будет стоить ни копейки.

В то же время vector::atпредоставляется, который всегда проверяет и выходит за границы даже в сборках релиза, но это снижает производительность до такой степени, что я часто вижу гораздо больше кода, vector::operator[]чем использую vector::at. Большая часть дизайна C ++ перекликается с идеей «плати за то, что ты используешь / нуждаешься», и многие люди часто одобряют operator[]это, даже не заботясь о проверке границ в сборках релизов, основываясь на том, что они надевают не нужно проверять границы в их оптимизированных сборках. Внезапно, если утверждения были включены в сборках выпуска, производительность этих двух была бы идентична, и использование вектора всегда заканчивалось бы медленнее, чем динамический массив. Таким образом, огромная часть дизайна и преимуществ утверждений основана на идее, что они становятся свободными в сборке релиза.

release_assert

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

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

На самом деле есть некоторые случаи, когда я обнаружил бы серьезный сбой с номером строки и сообщением об ошибке, предпочтительнее изящного восстановления после выброшенного исключения, которое может быть достаточно дешевым, чтобы его можно было сохранить в выпуске. И в некоторых случаях невозможно выполнить восстановление из исключения, например ошибка, возникшая при попытке восстановления из существующего. Там я бы нашел идеальную подгонку для release_assert(!"This should never, ever happen! The software failed to fail!");и, естественно, это было бы очень дешево, так как проверка была бы выполнена внутри исключительного пути в первую очередь и не стоила бы ничего в обычных путях выполнения.


Мое намерение не состояло в том, чтобы включить утверждения в стороннем коде, см. Уточняющее редактирование. Цитата из моего комментария опускает контекст из моего вопроса: Additionally, the performance argument for me only counts when it is a measurable problem.так что идея состоит в том, чтобы в каждом конкретном случае решать, когда вынести утверждение из релиза.
Никто

Одна из проблем, связанных с последним комментарием, заключается в том, что стандартная библиотека использует то же самое, assertкоторое использует те же определения _DEBUG/NDEBUGпрепроцессора, что и при использовании assertмакроса. Таким образом, нет способа выборочно включить утверждения только для одного фрагмента кода, не включая его и для стандартной библиотеки, например, предполагая, что она использует стандартную библиотеку.

Существует проверка итератора STL, которую можно отключить отдельно, что отключает некоторые из утверждений, но не все.

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

vector::operator[]и vector::at, например, он имел бы практически одинаковую производительность, если бы утверждения были включены в сборках релизов, и не было бы никакого смысла operator[]больше использовать с точки зрения производительности, так как он все равно будет выполнять проверку границ.

2

Я пишу на Ruby, а не на C / C ++, и поэтому я не буду говорить о разнице между утверждениями и исключениями, но я хотел бы поговорить об этом как о том, что останавливает среду выполнения . Я не согласен с большинством ответов, приведенных выше, поскольку остановка программы с печатью обратной трассировки прекрасно работает для меня как метод защитного программирования.
Если есть способ для подпрограммы assert (абсолютно независимо от того, как она синтаксически написана, и если слово «assert» используется или существует в языке программирования или вообще в dsl), то это означает, что некоторая работа должна быть выполнена и продукт немедленно возвращается от «выпущенного, готового к использованию» к «нужному исправлению» - теперь либо переписайте его для реальной обработки исключений, либо исправьте ошибку, из-за которой появлялись неправильные данные.

Я имею в виду, что Assert - это не то, с чем вам следует жить и часто звонить - это сигнал остановки, который указывает, что вы должны сделать что-то, чтобы это никогда не повторилось. И говорить, что «релизная сборка не должна иметь утверждений», все равно, что говорить «релизная сборка не должна иметь ошибок» - чувак, это почти невозможно, разберись с этим.
Или думайте о них как о «провальных юнит-тестах, выполненных и запущенных конечным пользователем». Вы не можете предсказать все, что пользователь сделает с вашей программой, но если что-то слишком серьезное пойдет не так, это должно прекратиться - это похоже на то, как вы строите конвейеры сборки - вы останавливаете процесс и не публикуете, не так ли? ? Утверждение заставляет пользователя останавливаться, сообщать и ждать вашей помощи.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.