Здравствуйте. Здравствуйте! Меня зовут Илья Шишков, и мы начинаем наш третий курс, который называется «Красный пояс по C++». Обратите внимание на мою футболку — теперь вы всегда будете знать, над получением какого пояса вы сейчас работаете. И первая тема нашего курса — это «Введение в макросы». Давайте с вами вспомним, что во втором курсе в «Желтом поясе по C++» мы с вами разработали Unit-test Framework для создания Unit-test'ов. Кроме того, у нас была задача, которая называлась «тестирование класса rational», в которой нужно было написать набор Unit-test'ов для класса rational, вот он представлен на экране, этот класс представлял собой рациональное число. И нам нужно было разработать набор Unit-test'ов, которые проверяли, что этот класс реализован корректно. И сейчас у меня представлена программа, которая тестирует класс rational с помощью нашего Unit-test Framework. В ней есть два теста: TestDefaultConstructor, который проверяет, как работает конструктор по умолчанию в классе rational, и TestConstruction. У нас есть один тест, который проверяет, как себя ведет класс rational, когда ему в конструктор передается числитель и знаменатель. И функцией main мы создаем объект класса TestRunner из нашего Framework и запускает два наших теста с помощью метода RunTest, передавая в него, собственно, тест и текстовое сообщение. Давайте скомпилируем, запустим нашу программу и видим, что она компилируется, запускается и оба теста отрабатывают корректно, и мы видим об этом сообщение в консоли. На что сейчас хочется обратить свое внимание: на эти строковые сообщения. Я напомню, что мы их добавляли в наши шаблоны AssertEqual с определенной целью: для того чтобы, когда наш assert срабатывает, понять, какой именно assert сработал. Вот давайте мы сейчас сделаем следующую вещь — мы пойдем в класс rational (он у нас сейчас работает) и допустим в нем какую-нибудь ошибку. Вот, например, вместо знаменателя будем возвращать числитель. Запустим наш код и увидим, что наши тесты упали и они выводят сообщение. Мы видим, что TestDefaultConstructor упал, и сообщение об assert'е говорит, что... Само сообщение гласит Default constructor denominator. Вот это сообщение мы использовали для того, чтобы взять его и поискать в нашем коде, и найти тот assert, который сработал, потому что он нам поможет понять, где же ошибка в программе. При этом нам надо было стараться делать все эти сообщения в каждом assert уникальными, что заставляло нас тратить время на придумывание этих самых сообщений. Кроме того, давайте посмотрим, как устроен вызов метода RunTest в классе TestRunner. Он принимает тестовую функцию, которая, собственно, выполняет тестирование, и еще строчку, которая, во всех наших примерах, и, я уверен, что во всех ваших программах, совпадала с именем функции. Зачем мы передаем эту строчку? Она нам нужна как раз-таки, чтобы формировать вот эти самые сообщения: TestDefaultConstructor fail, TestConstruction fail. То есть чтобы в консоль выводить имя теста, который либо прошел, либо не прошел. И это, опять же, неудобно, потому что это дублирование кода, нам надо здесь вставить имя функции и сюда ее тоже скопировать. Это неудобно. Вот давайте мы с вами поставим задачу избавиться от этих недостатков. Давайте мы постараемся получить следующую вещь: мы будем стремиться делать так, чтобы мы вызывали наш assert (AssertEqual или просто assert), просто передавая туда два аргумента (x или y в данном примере), и, если этот assert срабатывает, то на экран выводится, чему были равны аргументы x и y, а также в какой строчке какого файла этот assert находится, потому что этой информации нам достаточно для того, чтобы понять, какой assert работал. И она будет формироваться автоматически, от нас никаких действий она не потребует. И вторая вещь, которой мы хотим достичь, это мы хотим запускать наши Unit-test, просто передавая в метод RunTest саму функцию и не дублируя ее имя во втором параметре. То есть мы просто передаем саму функцию. Но при этом, если тест проходит, мы видим в консоли его имя, если он падает, то мы также видим его имя. Вот, давайте такую задачу себе поставим и попытаемся ее решить. Для того чтобы ее решить, давайте вспомним, что сборка проекта на C++ состоит из трех стадий: это препроцессинг, компиляция и компоновка. И мы с вами говорили в «Желтом поясе» о том, что на стадии препроцессинга выполняются директивы include, то есть, все подключаемые файлы, их содержимое копируется в тело компилируемого файла, и после этого наступает стадия компиляции. Так вот, на самом деле, на стадии препроцессинга происходят еще некоторые действия, помимо обработки директив include. И давайте посмотрим, что именно происходит. Для этого мы наш вот этот вот проект закроем и откроем просто проект с пустой функцией main. Так вот, на этапе препроцессинга помимо обработки директив include еще происходит разворачивание макросов. Что такое макросы? Они объявляются ключевым словом define, и тут для примера я могу написать define MY_MAIN int main, даже вот так, со скобочками. И, например, я могу завести еще один макрос, назвать его FINISH и написать return 0. Дальше я беру вот эту вот функцию int main и заменяю ее на MY_MAIN, а здесь я пишу FINISH. Я только что объявил два макроса, один из них MY_MAIN, другой — FINISH, которые должны будут на этапе препроцессинга развернуты в свое определение: вот в int main и return 0. То есть, на этапе препроцессинга, когда препроцессор встречает имя макроса, он берет, смотрит определение этого макроса и заменяет имя макроса на его определение. Во-первых, давайте убедимся, что это компилируется — это компилируется, это работает, у нас программа ничего не выводит, но она успешно завершилась. Давайте посмотрим, как это происходит. Итак, откроем консоль, у нас есть наш файл, наш исходный файл, который мы написали, с двумя макросами, и давайте применим к нему уже известную нам команду g++ −E, которую, собственно, выполняет только препроцессинг. И мы видим, что из нашего кода пропали объявления макросов, вот эти define, пропало их использование MY_MAIN и FINISH, но в нашем коде появилась функция main, которая выполняет return 0. Собственно, это пример показывает, что действительно макросы были развернуты, и MY_MAIN и FINISH заменились на их тела. Но это были макросы, не содержащие параметров. Давайте снова вернемся в наш проект с Unit-test'ами и объявим здесь такой достаточно простой макрос, который [БЕЗ_ЗВУКА] не будет добавлять ничего нового. [БЕЗ_ЗВУКА] Ну и давайте даже добавим вот эти параметры. Вот, я сейчас объявил макрос ASSERT_EQUAL большими буквами с тремя параметрами: x, y и m. И разворачивается он в вызов нашего шаблона AssertEqual с этими тремя параметрами. То есть я могу заменить вызов шаблона на макрос, вот я это сделал, моя программа компилируется, работает, да, мы там допустили ошибку, и наши тесты падают. И давайте снова посмотрим, как [БЕЗ_ЗВУКА] эти assert разворачиваются на этапе препроцессинга. Вот наш файл, в консоли мы его открыли, вот наш define, который мы только что сделали, и давайте точно так же [БЕЗ_ЗВУКА] выполним препроцессинг. Что мы видим? Мы видим, что вот у нас есть TestDefaultConstructor, наша функция, в этой самой функции TestDefaultConstructor мы применили assert equal в качестве макроса, и вот, вот он раскрылся в вызов функции, вызов нашего шаблона AssertEqual. Сейчас, конечно, мы никакой пользы от того, что мы обернули AssertEqual в макрос, не получили, происходит просто текстовая замена, и никакой пользы мы от этого не получаем, к которой мы на самом деле стремимся. Но далее мы посмотрим, как с помощью макросов достичь тех эффектов, которые мы хотим получить от нашего Unit-test Framework. Сейчас же давайте подведем итоги: в этом видео мы с вами познакомились с макросами, мы узнали, что они создаются с помощью ключевого слова define, и на этапе препроцессинга происходит текстовая замена имени макроса на его содержимое. При этом макросы могут не содержать параметров или содержать один или несколько параметров.