Уже давным-давно я работал в одной крупной компании в должности C++-разработчика и столкнулся с одной очень странной ошибкой. Я написал примерно такой класс:
class Foo {
static void* operator new() {
return ...;
};
};
И увидел огромный stack-trace ошибок о недопустимом вызове оператора в этом контекста (на тот момент я использовал MS Visual Studio 2013 и встроенный в него MSVC-компилятор). Я искал проблему часа два, и помогло мне только просматривание готовой единицы трансляции. Как вы могли догадаться, проблема была связана с препроцессором, но обо всём по порядку.
Препроцессор в C++ — это такая, на первый взгляд, очень простая штука, основное назначение которой — добавлять в ваш исходный файл куски кода до того, как им займётся компилятор (для знающих: препроцессор формирует единицы трансляции). Он подключает include-файлы, обрабатывает всякие там #pragma once
, но самая главная директива препроцессора, безусловно, это директива #define
. Она позволяет заменять один кусок текста на другой. Например, вы пишете #define foo goo
, и после этого на этапе работы препроцессора все упоминания отдельного токена (слова) foo
в вашем коде заменяются на goo
.
Это очень мощный инструмент, он поддерживает аргументы и даже раскрывает последовательности (правда, без рекурсии):
#define foo goo
#define goo doo
foo(); // тут вызовется doo
Единственная проблема — препроцессор очень тупой. Он совершенно не следит за тем, что именно вы define'ите. Например, можно сделать так: #define float double
, и это действительно заменит все float
на double
; или так: #define std qwerty
, и это заменит все упоминания пространства имён std
на qwerty
(std::cout
-> qwerty::cout
).
Препроцессор действует на уровне единицы трансляции, то есть #define
, объявленный в файле a.cpp, не будет действовать в файле b.cpp. Но там, где он действует, он будет действовать беспощадно :-) #define
, которого вы не ожидаете может очень сильно изменить логику работы программы и неприятно удивить. Ровно так и получилось в моём случае: в одном из include-файлов было такое объявление:
#define new DEBUG_NEW
а в другом include-файле, такое:
#define DEBUG_NEW new(__LINE__, __FILE__)
Что сделал наш волшебный препроцессор? Правильно, честно всё заменил, и в результате получилось вот так:
class Foo {
static void* operator new(__LINE__, __FILE__)() {
return ...;
};
};
Для чего нужен DEBUG_NEW
— вопрос отдельный. Коротко говоря, это отладочный оператор выделения памяти, ведущий учёт всех запрошенных блоков. Тем не менее, теперь ошибка синтаксиса языка уже становится очевидной. Однако это не отменяет того факта, что искать её было очень тяжело.
Успешной вам отладки :-)
Комментарии (20)
Sam86
09.12.2021 12:35+6Я выругался матом и меня оштрафовали!
Русский язык плохой, потому что позволяет так делать.
ncr
09.12.2021 13:13+4на тот момент я использовал MS Visual Studio 2013 и встроенный в него MSVC-компилятор
Оно же макросы подсвечивает другим цветом, что там искать 2 часа?LynXzp
09.12.2021 13:47+1Не знаю как автор, но когда я вижу десятки цветов подсветки то они ничего для меня уже не значат. Просто радуга, почти так. Раньше делал свои цветовые схемы, но надоело.
Просто воспринимаю как дополнение к красивому форматированию, блоки разделяет ну и ладно.DistortNeo
10.12.2021 01:58+2Вам достаточно видеть, что ключевое слово подсвечивается как-то странно. Это сложно не заметить.
mayorovp
10.12.2021 10:27+2Это очень легко не заметить, потому что от ключевого слова "странная" подсветка как раз ожидается, а точный цвет легко забыть.
NikSmirnov
09.12.2021 15:29+2По-моему тут камень в огород не к Define, а к Include и областям видимости. Несколько беспорядочных деклараций в хедере, потом несколько беспорядочных инклюдов (в идеале вложенных один в другой), и искать ошибки можно очень долго.
Кроме того, специально придуманы namespace-ы, чтобы ограничивать области видимости имен. Так же и с дефайнами — оригинальная замена лежит в глобальном скоупе и подменяется где надо, и где не надо.
А был бы свич, задающий "область видимости", т.к. на препроцессор namespace не распространяются, и вы бы в своем файле решали, какая версия new нужна.#ifdef USE_DEBUG_NEW #define new DEBUG_NEW #else #define new #endif
Недостаток — не работает в случае 3rd party библиотек, где уже постарался кто-то, об областях видимости не задумывающийся.
myrrc
09.12.2021 15:51+3В C++20 добавили __VA_OPT__, так что теперь рекурсия в макросах есть, пример использования: https://www.scs.stanford.edu/~dm/blog/va-opt.html
Ingulf
09.12.2021 17:12+1В современном С++ вроде макросы не очень хороший стиль, и модулями уже можно заменять беспорядочные include, зачем же вот это все((
nickolaym
10.12.2021 01:28В статье говорится, что #include рекурсивно из коробки...
Это что же получается, можно говнокодить в таком вот стиле?
// устанавливаем аргументы #define FOO foo #define BAR bar #define TIMES 100500 // и вызываем макропроцедуру #include "foobar_n_times.h" /// где foobar_n_times.h #if (TIMES) > 0 FOO BAR // какое-то содержательное действо с аргументами инклуда #define TIMES (TIMES-1) // ???? хз как это правильно сделать #include "foobar_n_times.h" #endif
nickolaym
09.12.2021 19:16+1Старая добрая грабелька от микрософта. Очень старая. На неё наступали ещё пользователи MSVC 6, если вообще не 4.
4eyes
10.12.2021 15:41+2#include <windows.h> int main(int argc, char* argv[]) { int x = 0; int y = 0; return max(++x, y); // return 2. "Why? Because fuck you, that's why." }
Старая, заезженная тема. Особенно интересно, когда аргументы макроса имеют побочные эффекты.
staticmain
А зачем вы использовали ключевое слово языка в качестве имени метода?
mayorovp
Э-э-э, а как ещё можно перегрузить оператор new без использования ключевого слова new?
zvszvs
А свой operator new в классе уже запретили объявлять?