Уже давным-давно я работал в одной крупной компании в должности 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)


  1. staticmain
    09.12.2021 11:30
    -13

    А зачем вы использовали ключевое слово языка в качестве имени метода?


    1. mayorovp
      09.12.2021 11:38
      +18

      Э-э-э, а как ещё можно перегрузить оператор new без использования ключевого слова new?


    1. zvszvs
      09.12.2021 12:20
      +2

      А свой operator new в классе уже запретили объявлять?


  1. justmara
    09.12.2021 11:31
    +6

    >i too like

    мгимо финишд


    1. LuggerMan
      09.12.2021 11:36
      +8

      >to to
      mgimo not even started


  1. Portid
    09.12.2021 12:20
    -8

    Хорошая статья!


  1. Sam86
    09.12.2021 12:35
    +6

    Я выругался матом и меня оштрафовали!

    Русский язык плохой, потому что позволяет так делать.


  1. ncr
    09.12.2021 13:13
    +4

    на тот момент я использовал MS Visual Studio 2013 и встроенный в него MSVC-компилятор

    Оно же макросы подсвечивает другим цветом, что там искать 2 часа?


    1. LynXzp
      09.12.2021 13:47
      +1

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


      1. DistortNeo
        10.12.2021 01:58
        +2

        Вам достаточно видеть, что ключевое слово подсвечивается как-то странно. Это сложно не заметить.


        1. mayorovp
          10.12.2021 10:27
          +2

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


  1. loginsin
    09.12.2021 14:06
    +7

    Ждал комментария:

    #define true false

    Но не увидел. Пришлось написать самому.


    1. gdt
      09.12.2021 16:48
      +8


  1. 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 библиотек, где уже постарался кто-то, об областях видимости не задумывающийся.


  1. myrrc
    09.12.2021 15:51
    +3

    В C++20 добавили __VA_OPT__, так что теперь рекурсия в макросах есть, пример использования: https://www.scs.stanford.edu/~dm/blog/va-opt.html


    1. Ingulf
      09.12.2021 17:12
      +1

      В современном С++ вроде макросы не очень хороший стиль, и модулями уже можно заменять беспорядочные include, зачем же вот это все((


    1. 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


  1. nickolaym
    09.12.2021 19:16
    +1

    Старая добрая грабелька от микрософта. Очень старая. На неё наступали ещё пользователи MSVC 6, если вообще не 4.


  1. igorjan94
    09.12.2021 19:53

    А ведь и gcc (начиная с 4.8.1, 2013 год) и clang (как минимум начиная с 3.0) показывают ошибку сразу и чётко. Я сначала думал, что проблема в "давным-давно", но msvc до сих пор пишет на такой код какую-то ерунду...


  1. 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."
    }

    Старая, заезженная тема. Особенно интересно, когда аргументы макроса имеют побочные эффекты.