Мы с вами рассмотрели светлую сторону макросов C++. Они позволили нам сделать наш код короче и проще в использовании. Но макросы имеют и тёмную сторону. Я уверен, что вы могли слышать рекомендации, что макросы в C++ — это зло и что никогда нельзя использовать их в своих программах. Да, действительно, у макросов есть тёмная сторона. И в этом видео мы её рассмотрим, чтобы вы были в курсе и знали, какие проблемы могут возникать при чрезмерном использовании макросов в вашем коде. Основная проблема состоит в том, что иногда при использовании макросов ваш код ведёт себя не так, как вы ожидаете от него, на первый взгляд. Давайте рассмотрим пример. Напишем макрос MAX, который будет находить максимум из двух своих аргументов. И мы его реализуем с помощью простого тернарного оператора. Давайте этим макросом воспользуемся. Объявим переменную x, равную четыре, и переменную y, равную два. А в переменную z присвоим максимум из x и y плюс пять. Ну и давайте выведем, чему у нас получилось равным z. Так, компилируем. Компилируется, всё нормально. Что мы ожидаем увидеть на экране? Ну максимум из четырёх и два — это четыре плюс пять. Должно быть девять. Запускаем нашу программу. И видим четыре. Странно, мы же девять обещали. Что-то явно пошло не так. Ну и, как мы уже не раз делали с макросами, давайте посмотрим, во что наш макрос раскрылся. Переключимся в консоль и выполним уже хорошо знакомую нам команду g++ -E. [ШУМ] И вот здесь вот внизу мы можем видеть, во что раскрылся наш макрос. Смотрите, как работает наша программа. Он проверяет, если x больше y, то в переменную z он записывает значение x. А если же это не так, то есть y больше либо равен, чем x, то он туда запишет y плюс пять. И теперь понятно, почему мы увидели на экране значение четыре. Потому что четыре больше двух, и поэтому z мы просто присвоили четыре. Как эту проблему исправить? Быстрый способ — это обернуть весь макрос в скобки. Тогда всё будет работать нормально, и мы увидим в консоли девятку. Но на самом деле, в данном случае вместо макроса гораздо лучше использовать функцию. Мы уже знаем, что в заголовочном файле «алгоритм» есть стандартный алгоритм MAX, который вычисляет максимум из двух своих аргументов. И в данном случае лучше использовать его, потому что он будет работать именно так, как мы от него ожидаем. Ну на самом деле, пример с макросом MAX достаточно искусственный. Трудно представить, зачем писать макрос, когда уже есть готовая функция в стандартной библиотеке. Давайте рассмотрим другой пример, чуть более реальный. В стандартной библиотеке C++ нет функции, которая возводит свой аргумент в квадрат. И мы можем захотеть себе такую функцию завести, но по каким-то причинам мы решили реализовать её с помощью макроса. Пишем макрос SQR с одним параметром x, который, собственно, x умножает на x. Причём, смотрите, мы уже учли урок предыдущего примера и макрос сразу обернули в скобки. Хорошо, давайте этим макросом воспользуемся. У нас будет одна переменная x, равная трём, и в переменную z мы запишем SQR от x плюс один. И выведем z. Код наш опять же компилируется. Ну чи что мы ожидаем? Мы ожидаем увидеть в консоли 16. Три плюс один — четыре в квадрате 16. Но почему-то видим семь. Опять в нашей программе что-то идёт не так. И мы уже знаем, как это выяснить — посмотреть в вывод препроцессора. Запускаем препроцессор. Идём вниз и смотрим, что макрос раскрылся вот в такое выражение x плюс один умножить на x плюс один. x это три, один на x — три, ещё единичка, три плюс три плюс один — семь. Хотя должно было быть 16. Снова мы допустили какую-то ошибку в макросе и снова у нас что-то работает не так. Как это исправить? Быстрое исправление — взять и обернуть x в скобки в теле макроса. Отлично, мы получили 16. Но по-хорошему вместо этого макроса нужно написать шаблон или функцию. Просто мы хотим написать универсальный шаблон возведения чего угодно в квадрат. [ШУМ] Вот мы пишем шаблон SQR, который прекрасно возводит x плюс один в квадрат и выдаёт нам 16. И он лишён недостатка. Мы не должны всё время помнить о том, что надо аргумент обернуть в скобки, надо весь макрос обернуть в скобки. Поэтому в данном случае вместо макроса лучше написать шаблон или функцию. Ну допустим, мы настырные, мы не хотим вместо макроса SQR писать функцию или шаблон, мы решили другую функцию написать, вот такую: LogAndReturn. Она принимает параметр x и делает следующее: она выводит сообщение «x равно параметру x» и возвращает свой параметр x. Такая вот функция. И мы решили вызов этой функции передать в качестве параметра макроса. Макроса, а не функции SQR. Что мы ожидаем? Ну мы ожидаем, что функция будет вызвана, в консоли выведется x равно три, а потом выведется девять. Потому что мы уже знаем, что в данном случае у нас в квадрат всё нормально возводится. Запускаем нашу программу и видим, что в консоль вместо одного раза сообщение «x = 3» выдалось дважды. Ну из вот этого кода, когда вы его читаете, вы вообще не ожидаете, что функция LogAndReturn будет вызвана дважды. Ну а что на самом деле происходит? Ну давайте посмотрим результат препроцессирования. А вот что происходит: наш макрос выполнил прямую текстовую замену и вызов функции LogAndReturn в код вставился дважды. И функция вместо того, чтобы вызваться только один раз, она вызывается дважды. И это как бы просто учебный пример. А представьте, эта функция в реальном коде выполняла бы какие-нибудь сложные, долгие вычисления. Мы на ровном месте могли бы получить себе просадку производительности просто из-за того, что неудачно воспользовались макросом. Ну хорошо, мы продолжаем упорствовать, продолжаем настырно хотеть пользоваться макросами и говорим: «Ладно, нечего в качестве аргумента макроса использовать результат вызова функции; ну ты же понимаешь, что ты макрос вызываешь? Ну сохрани результат вызова функции в отдельную переменную и передай эту переменную в макрос, всё будет нормально». Да, действительно, всё будет нормально, всё будет работать. Вот у нас «x равно три» выдался один раз, но есть другая проблема. Допустим, вот эта переменная x, она нам после вот этой четырнадцатой строчки понадобится. Причём нам понадобится её увеличить на один. И мы в целях экономии количества строк кода решили увеличить эту переменную вот прямо здесь. Ну мы передаём x, равный три, в SQR и сразу увеличиваем на единичку, чтобы потом работать с четвёркой. Ну как бы безобидно достаточно. Мы ожидаем. что сейчас всё равно вот здесь вот появится девятка. Запускаем наш код и почему-то здесь появилась не девятка, а 12. Опять что-то идёт не так. При этом в данном случае компилятор нам подсказывает, что мы явно делаем что-то не так. Вот посмотрите, нам компилятор пишет «operation on 'x' may be undefined». В чём тут дело? Снова идём в результат препроцессирования. [ШУМ] Смотрите, наш макрос раскрылся в выполнении x плюс плюс дважды в одном выражении. На самом деле, когда мы одну и ту же переменную несколько раз изменяем в одном и том же выражении, то результат не определён, потому что мы не знаем, какое из изменений — правое или левое в данном случае — будет выполнено первым. Тем не менее, несмотря на то, что компилятор нам выдал warning, мы, естественно, warning не прочитали, ну кто warning'и читает? Напоминаю, что всегда в боевом коде надо стараться компилировать свои программы с флажком минус w-error, чтобы warning'и трактовались как ошибки компиляции. Так вот. Вот из этого опять же вызова не очевидно, что x увеличивается на единицу больше, чем один раз в одном выражении. Поэтому опять же как от этого застраховаться? Вместо макроса в данном случае написать функцию или шаблон. Мы уже писали шаблон SQR и видели, как это сделать. Давайте подведём итоги. Использование макросов в программах может приводить к неожиданным ошибкам в поведении или же, в лучшем случае, к неожиданным ошибкам компиляции. Поэтому если вместо макроса можно написать функцию или шаблон с аналогичным поведением, то именно так и надо сделать. Вы защититесь от неожиданных ошибок при использовании макросов. Дальше. Поэтому если ваш макрос не использует специальные макросы FILE, LINE или оператор «решётка», то дважды подумайте, не стоит ли написать шаблон, если вы пишете макрос. Когда вы пишете макрос, который не использует FILE, LINE и оператор «решётка», дважды подумайте, нельзя ли его заменить на шаблон. Если же вы всё-таки осознанно пишете макрос и понимаете, почему и зачем вы это делаете, то старайтесь, во-первых, использовать каждый аргумент макроса ровно один раз, потому что если вы вставляете его несколкьо раз в код, то если этот аргумент — это вызов функции, то функция будет вызываться несколько раз. И, во-вторых, старайтесь максимально изолировать аргументы макросов с помощью скобок.