Макросы — один из самых мощных инструментов в языках программирования. В самом простом виде макрос, это символьное имя, которое заменяется на другое или целую последовательность программных инструкций, что позволяет упростить процесс написания кода при меньших затратах времени и усилий на кодирование, чем если бы программист писал все целиком вручную.
Возможности макросов зависят от конкретного языка программирования (макропроцессора) и некоторые из языков программирования позволяют себя расширять новыми синтаксическими конструкциями, фактически, реализуя парадигму 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)
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); }
Но не работают стандартные значения параметров
rsashka Автор
00.00.0000 00:00Класс, спасибо добрый человек! Примерно этого и хотелось.
Примерно тоже самое можно сотворить и с заменой переменных (хотя там и будут нюансы с с получением значений).
IvanPetrof
00.00.0000 00:00Непонятна ваша цель. Для чего вам это? Это ваш код? Легаси? Или не ваш? К примеру, если ваш и вы пишете проект с нуля, в коде которого нужна такая возможность, то можно просто условиться, что есть макрос foo() который используется для вызова, а есть парный ему foo_ для взятия адреса.
И вообще, зачем вам макрос со скобками?
#define foo new_foo
Прекрасно работает и так foo(), и так &foo
P.s.
Есть ещё такая вещь, как кодогенератор. По сути - это внешний макродвижок, который обрабатывает файл перед его компиляцией. Там вы можете какие угодно правила реализовать.
fk0
00.00.0000 00:00+1Если бы не одно но. Когда требуется взять адрес функции, то её имя нужно указывать без скобок! И если имя этой функции требуется переопределять с помощью макропроцессора, то тут вариант только один, определять нужно только одно имя без скобок и каких либо аргументов.
Имя функции -- это символ с определённым типом. Никаких ограничений на счёт скобок для него нет. Может быть сколько угодно круглых скобок...
Как не используя макросы препроцессора С/С++ можно переопределить имя функции как при её вызове, так и для операции взятия адреса функции?
Операция взятия адреса для функции скорей не нужна, функция автоматически превращается в указатель на функцию в C/C++...
Как-как. Если в C++, то можно с использованием ссылки на другую функцию. Ссылки с нужным именем.
Если в голом-C, то можно с использованием константного указателя на функцию.
Юнит-тест системы часто пользуются такими методами для mocking'а функций.
PS: если нужно что-то сделать с аргументами, то сделать это в своей промежуточной функции. Если что-то сделать с аргументами нужно в месте вызова, то очевидно, это не выполнимо без макроса (или, может быть, но не всегда, шаблона в C++). И если нужен именно макрос то задача скорей решения не имеет.
PPS: сформулируйте задачу точнее. А то может в упор не видно очевидного решения.
Apoheliy
00.00.0000 00:00+1В gcc можно сказать не использовать стандартный препроцессор и подпихнуть ему свой, который (например) может вызвать штатный препроцессор и потом добавить пару своих правок. Подробнее можно смотреть опцию -no-integrated-cpp (Preprocessor Options (Using the GNU Compiler Collection (GCC)) ). Из минусов - вы будете привязаны к собственной системе сборки.
Также есть (почти аналогичный) вариант: написать собственную утилиту, которая всё нужное подставит. Для примера смотрим moc на Qt5.
rsashka Автор
00.00.0000 00:00Своим препроцессором точно не хочется заморачиваться, но я не знал, что можно отключать штатный препроцессор у компилятора. Большое спасибо за эту наводку!
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/sGPY3PdYMrsashka Автор
00.00.0000 00:00Да, подобный вариант скорее всего должен помочь, и самое главное без использования препроцессора и перегрузки операций как в шаблонах!
fk0
00.00.0000 00:00+3Алиас достаточно бесполезная штука в данном случае. Он позволяет лишь создать второй символ который будет ссылаться на тот же адрес, что другой глобальный символ, уже определённый в этой единице трансляции. Такого же можно достичь без алиаса, с помощью asm-вставки: декларируем extern-символ (не обязательно функцию), а в асме его определяем где и как нужно. И его даже локальным сделать можно в асме. Но все трюки не позволят заменить один символ на другой и невозможно сделать алиас для не определённого в текущей единице трансляции символа.
Можно ещё воспользоваться программой objcopy и у ней есть опция позволяющая переименовывать символы. Соответственно, это можно сделать в мейке после компиляции, но до линковки. Такое обычно нужно, если есть конфликт по символам с внешней библиотекой.
Ещё может помочь опция линкера -Wl,--wrap=symbol, кстати. Все ссылки на symbol будут вести в __wrap_symbol (который нужно определить самостоятельно), а старую функцию можно вызвать как __real_symbol. Такое обычно используется когда нужно переопределить библиотечную функцию или делаются mock-функции.
rsashka Автор
00.00.0000 00:00Ещё может помочь опция линкера -Wl,--wrap=symbol, кстати. Все ссылки на
symbol будут вести в __wrap_symbol (который нужно определить
самостоятельно), а старую функцию можно вызвать как __real_symbol.
Такое обычно используется когда нужно переопределить библиотечную
функцию или делаются mock-функции.Во, большущее спасибо за хорошую мысль! Что-то я совсем про линкер не подумал, а ведь действительно замена символа должна решить эти проблемы!
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); }
rsashka Автор
00.00.0000 00:00Этот код будет работать, но нужно переопределять уже существущую функцию, а не делать новую функцию как ссылку на старую реализацию.
vamireh
00.00.0000 00:00-2Что за набор слов? Что такое "переопределять функцию", если необходим адрес "старой" функции?
fk0
00.00.0000 00:00Ссылка позволит иметь "альтернативное имя" для функции. И может ссылаться на переопределённую функцию. Что тут непонятного?
Идея в том, что например, переопределённую функцию сложно сделать static в пределах каждой единицы компиляции (чтоб не попадало в глобальные символы). А такую ссылку/указатель в каждую единицу компиляции положить можно. И в отдельную единицу компиляции функцию на которую она ссылается.
Тогда после линковки в коде может остаться и оригинальная функция, и новая, и весь код который использовал оригинальную функцию теперь будет вызывать новую. Ну тут нужно сделать так, чтоб у компилятора не было неоднозначности с определеним использовать ссылку или оригинальную функцию. Если оригинальная функция в хедерах не видна -- нет проблем. Если видна, то только при включении проблемного хедера в конкретном .c файле сделать #define function __function__, например, чтоб старая функция не конфликтовала с именем ссылки или указателя. И всё.
rsashka Автор
00.00.0000 00:00Основная проблема была в том, чтобы иметь новую функцию под старым именем. И чтобы для старого имени были доступны обращения как к функции (с указанием скобок), так и как к объекту для оператора получения адреса.
В приведенном вами примере наверно может получится реализовать переопределение с помощью статической функции, но мне кажется что получается неоправданно сложное решение.
vamireh
Написать другую функцию, вызывающую нужную
rsashka Автор
Это будет работать для вызова функции, но как получить адрес изначальной функции, ведь при операции взятия адреса будет возвращен адрес переопределенной функции foo, а не bar?
vamireh
Задача решена так, как она поставлена: и при вызове, и при взятии адреса.
А вообще, я занимаюсь программированием, а не извращениями в коде. Практическая польза данной "задачи" мне не ясна.
rsashka Автор
Решаемая задача - заменить в коде один идентификатор на другой во всех случаях возможного использования.
Это легко решается с помощью макросов, но в данном сценарии не получается с помощью инструментов, которые рекомендуется использовать вместо макросов.
aamonster
Пункт "refactor/rename" в IDE?
А вообще – рефакторить код, чтобы не хотелось странного. Ну а пока нужен хак – пользоваться макросами, просто минимизировать их количество и максимально упростить.
vamireh
А это вы вообще озвучиваете задачу рефакторинга.
Ниже показывают, что возможно. Но вот необходимость подобного извращения вами не объяснена.