![](https://habrastorage.org/getpro/habr/upload_files/fab/08e/941/fab08e941eafc81cd2f9b6f9f36e8813.png)
Уже давным-давно я работал в одной крупной компании в должности 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
— вопрос отдельный. Коротко говоря, это отладочный оператор выделения памяти, ведущий учёт всех запрошенных блоков. Тем не менее, теперь ошибка синтаксиса языка уже становится очевидной. Однако это не отменяет того факта, что искать её было очень тяжело.
Успешной вам отладки :-)