image


Макросы — один из самых мощных инструментов в языках программирования. В самом простом виде макрос, это символьное имя, которое заменяется на другое или целую последовательность программных инструкций, что позволяет упростить процесс написания кода при меньших затратах времени и усилий на кодирование, чем если бы программист писал все целиком вручную.


Возможности макросов зависят от конкретного языка программирования (макропроцессора) и некоторые из языков программирования позволяют себя расширять новыми синтаксическими конструкциями, фактически, реализуя парадигму DSL для конкретной решаемой задачи. Подобные возможности добавляют новые области применения и способы разработки больших программных системы, например, за счет использования языково-ориентированного программирования.


Если же говорить о простых реализациях макросов, например как для языков С и С++, то умные люди и умные книжки советуют избегать использования макросов и по возможности заменять их шаблонами, константами и inline-функциями. Ведь с помощью макросов можно не только упростить код, но и не менее изящно стрелять в ноги себе или своих товарищей.


И вот при окончательной доработке синтаксиса макросов для нового языка программирования я неожиданно столкнулся со сценарием, который элементарно реализуется с помощью макропроцессора C/C++, но который невозможно повторить при использовании любого из рекомендованных инструментов для их замены. И я буду очень рад, если ошибаюсь и кто-нибудь подскажет решение, которое можно сделать без применения макропроцессора.


Суть проблемы


Макросы можно определять как с аргументами, так и без аргументов. Причем макропроцессор считает макроопределение с аргументами и без оных идентичными. Поэтому невозможно сделать два #define со скобками и без них:


    #define MACRO   name
    #define MACRO() name() <-  error: 'MACRO' macro redefined [-Werror,-Wmacro-redefined]

Предположим, у нас есть переменная, имя которой нужно изменять во время компиляции, например, в зависимости от каких либо условий. И в общем виде это решается элементарно #define NAME new_name. То же самое можно сделать и для функции #define FUNC(...) func_name(__VA_ARGS__). А так как имена переменных и функций должны быть уникальны в своей области видимости, то и с определениями макросов, кажется, нет каких либо проблем, т.к. уникальность имен требуется как для препроцессора, так и для компилятора.


    #define FUNC(...) func_name(__VA_ARGS__)
    int func_name=0; <- error: redefinition of 'func_name' as different kind of symbol

Если бы не одно но. Когда требуется взять адрес функции, то её имя нужно указывать без скобок! И если имя этой функции требуется переопределять с помощью макропроцессора, то тут вариант только один, определять нужно только одно имя без скобок и каких либо аргументов.


    #define NAME(...)  name(__VA_ARGS__)

    int NAME(int arg=1){
        return arg;
    }

    int res = NAME();
    auto ref = &NAME; <- error: use of undeclared identifier 'NAME'

Рабочий вариант:


    #define NAME  name

    int NAME(int arg=1){
        return arg;
    }

    int res = NAME();
    auto ref = &NAME;

Внимание, вопрос.


Как не используя макросы препроцессора С/С++ можно переопределить имя функции как при её вызове, так и для операции взятия адреса функции? Мне кажется, что я перепробовал все варианты, но решения так и не нашел.


Update 1


Ниже в комментариях Ritan предложил частичное решение с помощью шаблона, в котором переопределяется оператор взятия адреса. И хотя подобный код не полностью соответствует изначально поставленной задаче, но максимально приближен к тому, чтобы хотелось получить в итоге.


Update 2


Еще более изящное решение вообще без макросов и переопределений подсказал fk0 в своем комментарии:


Ещё может помочь опция линкера -Wl,--wrap=symbol, кстати. Все ссылки на
symbol будут вести в __wrap_symbol (который нужно определить
самостоятельно), а старую функцию можно вызвать как __real_symbol.
Такое обычно используется когда нужно переопределить библиотечную
функцию или делаются mock-функции.

Комментарии (29)


  1. vamireh
    00.00.0000 00:00
    +2

    Как не используя макросы препроцессора С/С++ можно переопределить имя функции как при её вызове, так и для операции взятия адреса функции?

    Написать другую функцию, вызывающую нужную

    template<typename... Args>
    decltype(auto) foo(Args&&... args)
    {
      return bar(std::forward<Args>(args)...);
    }


    1. rsashka Автор
      00.00.0000 00:00

      Это будет работать для вызова функции, но как получить адрес изначальной функции, ведь при операции взятия адреса будет возвращен адрес переопределенной функции foo, а не bar?


      1. vamireh
        00.00.0000 00:00
        +2

        Задача решена так, как она поставлена: и при вызове, и при взятии адреса.

        А вообще, я занимаюсь программированием, а не извращениями в коде. Практическая польза данной "задачи" мне не ясна.


        1. rsashka Автор
          00.00.0000 00:00
          -1

          Решаемая задача - заменить в коде один идентификатор на другой во всех случаях возможного использования.

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


          1. aamonster
            00.00.0000 00:00
            +1

            Пункт "refactor/rename" в IDE?

            А вообще – рефакторить код, чтобы не хотелось странного. Ну а пока нужен хак – пользоваться макросами, просто минимизировать их количество и максимально упростить.


          1. vamireh
            00.00.0000 00:00

            Решаемая задача - заменить в коде один идентификатор на другой во всех случаях возможного использования.

            А это вы вообще озвучиваете задачу рефакторинга.

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

            Ниже показывают, что возможно. Но вот необходимость подобного извращения вами не объяснена.


  1. a-tk
    00.00.0000 00:00

    Сделать два разных макроса.


    1. rsashka Автор
      00.00.0000 00:00

      Если применять макросы, то и одного будет достаточно. Вопрос в том, как это сделать без макросов.


  1. Ritan
    00.00.0000 00:00
    +4

    #include <cstdio>
    #include <utility>
    
    template<typename R, typename ...Args>
    struct FunctionWrapper {
        using FunctionType = R(*)(Args...);
    
        FunctionWrapper(FunctionType func) : _func(func) {}
    
        R operator()(Args... args) {
          return _func(std::forward<Args...>(args...));
        }
    
        FunctionType operator &() {
          return _func;
        }
    
        FunctionType _func;
    };
    
    int foo(int arg){
      return arg;
    }
    
    FunctionWrapper bar{foo};
    
    int main() {
      printf("%p\n", &foo);
      printf("%p\n", &bar);
    }

    Но не работают стандартные значения параметров


    1. rsashka Автор
      00.00.0000 00:00

      Класс, спасибо добрый человек! Примерно этого и хотелось.

      Примерно тоже самое можно сотворить и с заменой переменных (хотя там и будут нюансы с с получением значений).


  1. aamonster
    00.00.0000 00:00
    +1

    Если отказаться от ненужного & для получения адреса функции – кажется, задача заметно упростится.


    1. rsashka Автор
      00.00.0000 00:00

      Согласен, но по начальным условиям он нужен :-(


  1. IvanPetrof
    00.00.0000 00:00

    Непонятна ваша цель. Для чего вам это? Это ваш код? Легаси? Или не ваш? К примеру, если ваш и вы пишете проект с нуля, в коде которого нужна такая возможность, то можно просто условиться, что есть макрос foo() который используется для вызова, а есть парный ему foo_ для взятия адреса.

    И вообще, зачем вам макрос со скобками?

    #define foo new_foo

    Прекрасно работает и так foo(), и так &foo

    P.s.

    Есть ещё такая вещь, как кодогенератор. По сути - это внешний макродвижок, который обрабатывает файл перед его компиляцией. Там вы можете какие угодно правила реализовать.


    1. rsashka Автор
      00.00.0000 00:00

      #define foo new_foo

      Сейчас именно так и сделано


  1. fk0
    00.00.0000 00:00
    +1

    Если бы не одно но. Когда требуется взять адрес функции, то её имя нужно указывать без скобок! И если имя этой функции требуется переопределять с помощью макропроцессора, то тут вариант только один, определять нужно только одно имя без скобок и каких либо аргументов.

    Имя функции -- это символ с определённым типом. Никаких ограничений на счёт скобок для него нет. Может быть сколько угодно круглых скобок...

    Как не используя макросы препроцессора С/С++ можно переопределить имя функции как при её вызове, так и для операции взятия адреса функции?

    Операция взятия адреса для функции скорей не нужна, функция автоматически превращается в указатель на функцию в C/C++...

    Как-как. Если в C++, то можно с использованием ссылки на другую функцию. Ссылки с нужным именем.

    Если в голом-C, то можно с использованием константного указателя на функцию.

    Юнит-тест системы часто пользуются такими методами для mocking'а функций.

    PS: если нужно что-то сделать с аргументами, то сделать это в своей промежуточной функции. Если что-то сделать с аргументами нужно в месте вызова, то очевидно, это не выполнимо без макроса (или, может быть, но не всегда, шаблона в C++). И если нужен именно макрос то задача скорей решения не имеет.

    PPS: сформулируйте задачу точнее. А то может в упор не видно очевидного решения.


  1. Apoheliy
    00.00.0000 00:00
    +1

    В gcc можно сказать не использовать стандартный препроцессор и подпихнуть ему свой, который (например) может вызвать штатный препроцессор и потом добавить пару своих правок. Подробнее можно смотреть опцию -no-integrated-cpp (Preprocessor Options (Using the GNU Compiler Collection (GCC)) ). Из минусов - вы будете привязаны к собственной системе сборки.

    Также есть (почти аналогичный) вариант: написать собственную утилиту, которая всё нужное подставит. Для примера смотрим moc на Qt5.


    1. rsashka Автор
      00.00.0000 00:00

      Своим препроцессором точно не хочется заморачиваться, но я не знал, что можно отключать штатный препроцессор у компилятора. Большое спасибо за эту наводку!


  1. PkXwmpgN
    00.00.0000 00:00
    +1

    Еще вариант посмотреть на средства для алиасинга символов на стороне конкретного компилятора. Эти штуки пока не стандартизированы, но есть предложения, например, https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2729.htm

    extern "C" int foo(int a) {
        return a;
    }
    
    #ifdef _WIN32
    #pragma comment(linker, "/alternatename:bar=foo")
    int bar(int);
    #else
    int bar(int a) __attribute__((alias("foo")));
    #endif
    
    int main() {
        foo(1);
        bar(1);
        assert(&foo == &bar);
    }

    Но там есть свои нюансы. В С++ на GCC/Clang нужно будет указывать mangled имя https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html.

    https://godbolt.org/z/sGPY3PdYM


    1. rsashka Автор
      00.00.0000 00:00

      Да, подобный вариант скорее всего должен помочь, и самое главное без использования препроцессора и перегрузки операций как в шаблонах!


    1. fk0
      00.00.0000 00:00
      +3

      Алиас достаточно бесполезная штука в данном случае. Он позволяет лишь создать второй символ который будет ссылаться на тот же адрес, что другой глобальный символ, уже определённый в этой единице трансляции. Такого же можно достичь без алиаса, с помощью asm-вставки: декларируем extern-символ (не обязательно функцию), а в асме его определяем где и как нужно. И его даже локальным сделать можно в асме. Но все трюки не позволят заменить один символ на другой и невозможно сделать алиас для не определённого в текущей единице трансляции символа.

      Можно ещё воспользоваться программой objcopy и у ней есть опция позволяющая переименовывать символы. Соответственно, это можно сделать в мейке после компиляции, но до линковки. Такое обычно нужно, если есть конфликт по символам с внешней библиотекой.

      Ещё может помочь опция линкера -Wl,--wrap=symbol, кстати. Все ссылки на symbol будут вести в __wrap_symbol (который нужно определить самостоятельно), а старую функцию можно вызвать как __real_symbol. Такое обычно используется когда нужно переопределить библиотечную функцию или делаются mock-функции.


      1. rsashka Автор
        00.00.0000 00:00

        Ещё может помочь опция линкера -Wl,--wrap=symbol, кстати. Все ссылки на
        symbol будут вести в __wrap_symbol (который нужно определить
        самостоятельно), а старую функцию можно вызвать как __real_symbol.
        Такое обычно используется когда нужно переопределить библиотечную
        функцию или делаются mock-функции.

        Во, большущее спасибо за хорошую мысль! Что-то я совсем про линкер не подумал, а ведь действительно замена символа должна решить эти проблемы!


  1. firegurafiku
    00.00.0000 00:00

    Мне кажется, что я перепробовал все варианты, но решения так и не нашел.

    А пробовали, как выше предлагает @fk0, объявить ссылку на функцию? Как-то так:

    #include <cstdio>
    
    int foo(float x) {
        return 0;
    }
    
    constexpr const auto& bar = foo;
    
    int main() {
        foo(1.0f);
        bar(1.0f);
        std::printf("%p\n", foo);
        std::printf("%p\n", bar);
        std::printf("%p\n", &foo);
        std::printf("%p\n", bar);
    }


    1. rsashka Автор
      00.00.0000 00:00

      Этот код будет работать, но нужно переопределять уже существущую функцию, а не делать новую функцию как ссылку на старую реализацию.


      1. vamireh
        00.00.0000 00:00
        -2

        Что за набор слов? Что такое "переопределять функцию", если необходим адрес "старой" функции?


      1. fk0
        00.00.0000 00:00

        Ссылка позволит иметь "альтернативное имя" для функции. И может ссылаться на переопределённую функцию. Что тут непонятного?

        Идея в том, что например, переопределённую функцию сложно сделать static в пределах каждой единицы компиляции (чтоб не попадало в глобальные символы). А такую ссылку/указатель в каждую единицу компиляции положить можно. И в отдельную единицу компиляции функцию на которую она ссылается.

        Тогда после линковки в коде может остаться и оригинальная функция, и новая, и весь код который использовал оригинальную функцию теперь будет вызывать новую. Ну тут нужно сделать так, чтоб у компилятора не было неоднозначности с определеним использовать ссылку или оригинальную функцию. Если оригинальная функция в хедерах не видна -- нет проблем. Если видна, то только при включении проблемного хедера в конкретном .c файле сделать #define function __function__, например, чтоб старая функция не конфликтовала с именем ссылки или указателя. И всё.


        1. rsashka Автор
          00.00.0000 00:00

          Основная проблема была в том, чтобы иметь новую функцию под старым именем. И чтобы для старого имени были доступны обращения как к функции (с указанием скобок), так и как к объекту для оператора получения адреса.

          В приведенном вами примере наверно может получится реализовать переопределение с помощью статической функции, но мне кажется что получается неоправданно сложное решение.


  1. AndGry
    00.00.0000 00:00
    +1

    Была похожая проблема. Отладка сгенерированных макросами методов - то ещё БДСМ. Тупо заменил макросы prebuild bash скриптом и воткнул его в свою систему сборки.


    1. rsashka Автор
      00.00.0000 00:00

      Спасибо за поддержку! Как я тебя понимаю.


  1. Woodroof
    00.00.0000 00:00

    namespace v1
    {
    int func();
    }
    inline namespace v2
    {
    int func();
    }