Когда полезны макросы C++?
Сообщество C++ вполне обоснованно опасается препроцессора Си. Встроенные функции, константы и шаблоны обычно являются более безопасной и превосходной альтернативой #define
,
Следующий макрос:
#define SUCCEEDED(hr) ((HRESULT)(hr) >= 0)
ни в коем случае не превосходит тип сейфа:
inline bool succeeded(int hr) { return hr >= 0; }
Но макросы имеют свое место, пожалуйста, перечислите виды использования макросов, которые вы не можете сделать без препроцессора.
Пожалуйста, поместите каждый вариант использования в отдельный ответ, чтобы за него проголосовали, и, если вы знаете, как получить один из ответов без препроцессора, укажите, как в комментариях этого ответа.
38 ответов
Как обертки для функций отладки, чтобы автоматически передавать такие вещи, как __FILE__
, __LINE__
, так далее:
#ifdef ( DEBUG )
#define M_DebugLog( msg ) std::cout << __FILE__ << ":" << __LINE__ << ": " << msg
#else
#define M_DebugLog( msg )
#endif
Методы всегда должны быть полным, компилируемым кодом; Макросы могут быть фрагментами кода. Таким образом, вы можете определить макрос foreach:
#define foreach(list, index) for(index = 0; index < list.size(); index++)
И используйте это так:
foreach(cookies, i)
printf("Cookie: %s", cookies[i]);
Начиная с C++11, это заменяется основанным на диапазоне циклом for.
Защита заголовочного файла требует макросов.
Есть ли другие области, которые требуют макросов? Не много (если есть).
Существуют ли другие ситуации, в которых полезны макросы? ДА!!!
Одним из мест, где я использую макросы, является очень повторяющийся код. Например, при переносе кода C++ для использования с другими интерфейсами (.NET, COM, Python и т. Д.) Мне нужно отлавливать различные типы исключений. Вот как я это делаю:
#define HANDLE_EXCEPTIONS \
catch (::mylib::exception& e) { \
throw gcnew MyDotNetLib::Exception(e); \
} \
catch (::std::exception& e) { \
throw gcnew MyDotNetLib::Exception(e, __LINE__, __FILE__); \
} \
catch (...) { \
throw gcnew MyDotNetLib::UnknownException(__LINE__, __FILE__); \
}
Я должен поставить эти защелки в каждой функции оболочки. Вместо того, чтобы каждый раз печатать полные блоки catch, я просто набираю:
void Foo()
{
try {
::mylib::Foo()
}
HANDLE_EXCEPTIONS
}
Это также облегчает обслуживание. Если мне когда-либо понадобится добавить новый тип исключения, мне нужно добавить только одно место.
Есть и другие полезные примеры: многие из них включают __FILE__
а также __LINE__
макросы препроцессора.
В любом случае, макросы очень полезны при правильном использовании. Макросы не являются злом - их неправильное использование является злом.
В основном:
- Включить охранников
- Условная компиляция
- Отчетность (предопределенные макросы, такие как
__LINE__
а также__FILE__
) - (редко) Дублирование повторяющихся шаблонов кода.
- В коде вашего конкурента.
Внутри условной компиляции, чтобы преодолеть проблемы различий между компиляторами:
#ifdef ARE_WE_ON_WIN32
#define close(parm1) _close (parm1)
#define rmdir(parm1) _rmdir (parm1)
#define mkdir(parm1, parm2) _mkdir (parm1)
#define access(parm1, parm2) _access(parm1, parm2)
#define create(parm1, parm2) _creat (parm1, parm2)
#define unlink(parm1) _unlink(parm1)
#endif
Если вы хотите сделать строку из выражения, лучшим примером для этого является assert
(#x
превращает значение x
в строку).
#define ASSERT_THROW(condition) \
if (!(condition)) \
throw std::exception(#condition " is false");
Строковые константы иногда лучше определить как макросы, так как вы можете делать больше с строковыми литералами, чем с const char *
,
Например, строковые литералы могут быть легко объединены.
#define BASE_HKEY "Software\\Microsoft\\Internet Explorer\\"
// Now we can concat with other literals
RegOpenKey(HKEY_CURRENT_USER, BASE_HKEY "Settings", &settings);
RegOpenKey(HKEY_CURRENT_USER, BASE_HKEY "TypedURLs", &URLs);
Если const char *
Тогда для выполнения конкатенации во время выполнения пришлось бы использовать какой-то класс строки.
const char* BaseHkey = "Software\\Microsoft\\Internet Explorer\\";
RegOpenKey(HKEY_CURRENT_USER, (string(BaseHkey) + "Settings").c_str(), &settings);
RegOpenKey(HKEY_CURRENT_USER, (string(BaseHkey) + "TypedURLs").c_str(), &URLs);
Когда вы хотите изменить поток программы (return
, break
а также continue
) код в функции ведет себя не так, как код, который фактически встроен в функцию.
#define ASSERT_RETURN(condition, ret_val) \
if (!(condition)) { \
assert(false && #condition); \
return ret_val; }
// should really be in a do { } while(false) but that's another discussion.
Очевидное включает охранников
#ifndef MYHEADER_H
#define MYHEADER_H
...
#endif
Допустим, мы будем игнорировать очевидные вещи, такие как охрана заголовков.
Иногда вы хотите сгенерировать код, который должен быть скопирован / вставлен прекомпилятором:
#define RAISE_ERROR_STL(p_strMessage) \
do \
{ \
try \
{ \
std::tstringstream strBuffer ; \
strBuffer << p_strMessage ; \
strMessage = strBuffer.str() ; \
raiseSomeAlert(__FILE__, __FUNCSIG__, __LINE__, strBuffer.str().c_str()) \
} \
catch(...){} \
{ \
} \
} \
while(false)
что позволяет вам кодировать это:
RAISE_ERROR_STL("Hello... The following values " << i << " and " << j << " are wrong") ;
И может генерировать сообщения, такие как:
Error Raised:
====================================
File : MyFile.cpp, line 225
Function : MyFunction(int, double)
Message : "Hello... The following values 23 and 12 are wrong"
Обратите внимание, что смешивание шаблонов с макросами может привести к еще лучшим результатам (т.е. автоматически генерировать значения рядом с именами переменных)
В других случаях вам понадобится __FILE__ и / или __LINE__ некоторого кода, например, для генерации отладочной информации. Ниже приведена классика для Visual C++:
#define WRNG_PRIVATE_STR2(z) #z
#define WRNG_PRIVATE_STR1(x) WRNG_PRIVATE_STR2(x)
#define WRNG __FILE__ "("WRNG_PRIVATE_STR1(__LINE__)") : ------------ : "
Как и в следующем коде:
#pragma message(WRNG "Hello World")
он генерирует сообщения вроде:
C:\my_project\my_cpp_file.cpp (225) : ------------ Hello World
В других случаях вам нужно сгенерировать код с помощью операторов конкатенации # и ##, таких как генерация геттеров и сеттеров для свойства (это в довольно ограниченных случаях).
В других случаях вы будете генерировать код, который не будет компилироваться, если он используется через функцию, например:
#define MY_TRY try{
#define MY_CATCH } catch(...) {
#define MY_END_TRY }
Который может быть использован как
MY_TRY
doSomethingDangerous() ;
MY_CATCH
tryToRecoverEvenWithoutMeaningfullInfo() ;
damnThoseMacros() ;
MY_END_TRY
(тем не менее, я только один раз видел, что этот вид кода используется правильно)
Наконец, что не менее важно, знаменитый boost::foreach
!!!
#include <string>
#include <iostream>
#include <boost/foreach.hpp>
int main()
{
std::string hello( "Hello, world!" );
BOOST_FOREACH( char ch, hello )
{
std::cout << ch;
}
return 0;
}
(Примечание: код копируется / вставляется с домашней страницы буста)
Что (ИМХО) намного лучше, чем std::for_each
,
Таким образом, макросы всегда полезны, потому что они находятся за пределами нормальных правил компилятора. Но я обнаружил, что в большинстве случаев они фактически остаются остатками кода C, никогда не переводимого в надлежащий C++.
Вы не можете выполнить короткое замыкание аргументов вызова функции, используя обычный вызов функции. Например:
#define andm(a, b) (a) && (b)
bool andf(bool a, bool b) { return a && b; }
andm(x, y) // short circuits the operator so if x is false, y would not be evaluated
andf(x, y) // y will always be evaluated
Фреймворки модульного тестирования для C++, такие как UnitTest ++, в значительной степени вращаются вокруг макросов препроцессора. Несколько строк кода модульного теста расширяются в иерархию классов, которые было бы неудобно печатать вручную. Без чего-то вроде UnitTest ++ и его волшебства препроцессора, я не знаю, как бы вы эффективно написали модульные тесты для C++.
Бояться препроцессора С - все равно, что бояться ламп накаливания только потому, что мы получаем люминесцентные лампы. Да, первое может быть {электричество | время программиста} неэффективно. Да, вы можете (буквально) сжечь их. Но они могут выполнить работу, если вы правильно с ней справитесь.
Когда вы программируете встраиваемые системы, C обычно является единственным вариантом, кроме ассемблера форм. После программирования на настольном компьютере с использованием C++, а затем перехода к более мелким встроенным целям вы научитесь перестать беспокоиться о "неэффективности" стольких голых функций C (включая макросы) и просто попытаетесь найти лучшее и безопасное использование, которое вы можете получить из этих функции.
Александр Степанов говорит:
Когда мы программируем на C++, нам не должно быть стыдно за наследие C, но мы должны в полной мере использовать его. Единственные проблемы с C++ и даже единственные проблемы с C возникают, когда они сами не согласуются с их собственной логикой.
Некоторые очень продвинутые и полезные вещи все еще могут быть собраны с использованием препроцессора (макросов), что вы никогда не сможете сделать с помощью "языковых конструкций" C++, включая шаблоны.
Примеры:
Создание чего-то и идентификатора C, и строки
Простой способ использовать переменные перечислимых типов в виде строки в C
Повторение кода.
Взгляните, чтобы увеличить библиотеку препроцессора, это своего рода мета-метапрограммирование. В теме-> мотивация вы можете найти хороший пример.
Мы используем __FILE__
а также __LINE__
макросы для диагностических целей при формировании исключений, захвате и ведении журнала, а также автоматические сканеры файлов журналов в нашей инфраструктуре контроля качества.
Например, бросающий макрос OUR_OWN_THROW
может использоваться с типом исключения и параметрами конструктора для этого исключения, включая текстовое описание. Как это:
OUR_OWN_THROW(InvalidOperationException, (L"Uninitialized foo!"));
Этот макрос конечно бросит InvalidOperationException
исключение с описанием в качестве параметра конструктора, но оно также записывает сообщение в файл журнала, состоящий из имени файла и номера строки, в которой произошел выброс, и его текстового описания. Сгенерированное исключение получит идентификатор, который также будет зарегистрирован. Если исключение когда-либо обнаруживается где-то еще в коде, оно будет помечено как таковое, и в файле журнала будет указано, что это конкретное исключение было обработано и, следовательно, оно вряд ли является причиной какого-либо сбоя, который может быть зарегистрирован позже. Необработанные исключения могут быть легко обнаружены нашей автоматизированной инфраструктурой контроля качества.
Я иногда использую макросы, чтобы определить информацию в одном месте, но по-разному использовать ее в разных частях кода. Это только слегка зло:)
Например, в "field_list.h":
/*
* List of fields, names and values.
*/
FIELD(EXAMPLE1, "first example", 10)
FIELD(EXAMPLE2, "second example", 96)
FIELD(ANOTHER, "more stuff", 32)
...
#undef FIELD
Тогда для публичного перечисления может быть определено просто использовать имя:
#define FIELD(name, desc, value) FIELD_ ## name,
typedef field_ {
#include "field_list.h"
FIELD_MAX
} field_en;
А в частной функции инициализации все поля можно использовать для заполнения таблицы данными:
#define FIELD(name, desc, value) \
table[FIELD_ ## name].desc = desc; \
table[FIELD_ ## name].value = value;
#include "field_list.h"
Одно из распространенных применений - это обнаружение среды компиляции. Для кроссплатформенной разработки вы можете написать, скажем, один набор кода для linux, а другой - для окон, когда для ваших целей уже не существует кроссплатформенной библиотеки.
Таким образом, в грубом примере кросс-платформенный мьютекс может иметь
void lock()
{
#ifdef WIN32
EnterCriticalSection(...)
#endif
#ifdef POSIX
pthread_mutex_lock(...)
#endif
}
Для функций они полезны, когда вы хотите явно игнорировать безопасность типов. Например, множество примеров выше и ниже для выполнения ASSERT. Конечно, как и многие функции C/C++, вы можете выстрелить себе в ногу, но язык дает вам инструменты и позволяет вам решать, что делать.
Что-то вроде
void debugAssert(bool val, const char* file, int lineNumber);
#define assert(x) debugAssert(x,__FILE__,__LINE__);
Так что вы можете просто, например, иметь
assert(n == true);
и получите имя исходного файла и номер строки проблемы, распечатанной в вашем журнале, если n равно false.
Если вы используете обычный вызов функции, такой как
void assert(bool val);
вместо макроса все, что вы можете получить, это номер строки вашей функции assert, напечатанный в журнале, что было бы менее полезно.
#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])
В отличие от "предпочтительного" шаблонного решения, обсуждаемого в текущем потоке, вы можете использовать его как константное выражение:
char src[23];
int dest[ARRAY_SIZE(src)];
Я использую макросы, чтобы легко определить исключения:
DEF_EXCEPTION(RessourceNotFound, "Ressource not found")
где DEF_EXCEPTION это
#define DEF_EXCEPTION(A, B) class A : public exception\
{\
public:\
virtual const char* what() const throw()\
{\
return B;\
};\
}\
Вы можете использовать #defines для помощи в сценариях отладки и модульного тестирования. Например, создайте специальные варианты регистрации функций памяти и создайте специальный memlog_preinclude.h:
#define malloc memlog_malloc
#define calloc memlog calloc
#define free memlog_free
Скомпилируйте ваш код, используя:
gcc -Imemlog_preinclude.h ...
Ссылка в вашем memlog.o на окончательное изображение. Теперь вы управляете malloc и т. Д., Возможно, для целей регистрации или для имитации сбоев выделения для модульных тестов.
Когда вы принимаете решение во время компиляции по конкретному поведению компилятора / ОС / оборудования.
Это позволяет вам сделать ваш интерфейс со спецификой Comppiler/OS/Hardware.
#if defined(MY_OS1) && defined(MY_HARDWARE1)
#define MY_ACTION(a,b,c) doSothing_OS1HW1(a,b,c);}
#elif define(MY_OS1) && defined(MY_HARDWARE2)
#define MY_ACTION(a,b,c) doSomthing_OS1HW2(a,b,c);}
#elif define(MY_SUPER_OS)
/* On this hardware it is a null operation */
#define MY_ACTION(a,b,c)
#else
#error "PLEASE DEFINE MY_ACTION() for this Compiler/OS/HArdware configuration"
#endif
Возможно, лучшее использование макросов находится в независящей от платформы разработке. Подумайте о случаях несоответствия типов - с помощью макросов вы можете просто использовать разные заголовочные файлы, например: --WIN_TYPES.H
typedef ...some struct
--POSIX_TYPES.h
typedef ...some another struct
--program.h
#ifdef WIN32
#define TYPES_H "WINTYPES.H"
#else
#define TYPES_H "POSIX_TYPES.H"
#endif
#include TYPES_H
На мой взгляд, гораздо удобнее, чем реализовывать это другими способами.
Компиляторы могут отклонить ваш запрос в строке.
Макросы всегда будут на своем месте.
Что-то, что я считаю полезным, это #define DEBUG для трассировки отладки - вы можете оставить его равным 1 во время отладки проблемы (или даже оставить его включенным в течение всего цикла разработки), а затем выключить его, когда придет время доставки.
Кажется, что VA_ARGS упоминается только косвенно:
При написании универсального кода C++03, когда вам нужно переменное количество (универсальных) параметров, вы можете использовать макрос вместо шаблона.
#define CALL_RETURN_WRAPPER(FnType, FName, ...) \
if( FnType theFunction = get_op_from_name(FName) ) { \
return theFunction(__VA_ARGS__); \
} else { \
throw invalid_function_name(FName); \
} \
/**/
Примечание. Как правило, проверка имени / броска также может быть включена в гипотетический get_op_from_name
функция. Это всего лишь пример. Может быть другой общий код, окружающий вызов VA_ARGS.
Как только мы получим шаблоны с переменным числом символов в C++11, мы сможем решить это "правильно" с помощью шаблона.
На моей последней работе я работал на антивирусном сканере. Чтобы мне было легче отлаживать, у меня было повсеместно много журналирования, но в таком приложении с высокими требованиями затраты на вызов функции просто слишком дороги. Итак, я придумал этот маленький макрос, который все еще позволял мне включать ведение журнала отладки в версии выпуска на сайте клиента, без затрат на вызов функции, проверял бы флаг отладки и просто возвращался без регистрации чего-либо, или, если он был включен, будет делать запись... Макрос был определен следующим образом:
#define dbgmsg(_FORMAT, ...) if((debugmsg_flag & 0x00000001) || (debugmsg_flag & 0x80000000)) { log_dbgmsg(_FORMAT, __VA_ARGS__); }
Из-за VA_ARGS в функциях журнала это был хороший случай для такого макроса.
До этого я использовал макрос в приложении с высоким уровнем безопасности, который должен был сказать пользователю, что у него нет правильного доступа, и он сообщал бы им, какой флаг ему нужен.
Макрос (ы) определены как:
#define SECURITY_CHECK(lRequiredSecRoles) if(!DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, true)) return
#define SECURITY_CHECK_QUIET(lRequiredSecRoles) (DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, false))
Затем мы можем просто распределить проверки по всему пользовательскому интерфейсу, и он скажет вам, каким ролям разрешено выполнять действие, которое вы пытались выполнить, если у вас еще не было этой роли. Причиной для двух из них было возвращение значения в некоторых местах и возврат из функции void в других...
SECURITY_CHECK(ROLE_BUSINESS_INFORMATION_STEWARD | ROLE_WORKER_ADMINISTRATOR);
LRESULT CAddPerson1::OnWizardNext()
{
if(m_Role.GetItemData(m_Role.GetCurSel()) == parent->ROLE_EMPLOYEE) {
SECURITY_CHECK(ROLE_WORKER_ADMINISTRATOR | ROLE_BUSINESS_INFORMATION_STEWARD ) -1;
} else if(m_Role.GetItemData(m_Role.GetCurSel()) == parent->ROLE_CONTINGENT) {
SECURITY_CHECK(ROLE_CONTINGENT_WORKER_ADMINISTRATOR | ROLE_BUSINESS_INFORMATION_STEWARD | ROLE_WORKER_ADMINISTRATOR) -1;
}
...
В любом случае, вот как я их использовал, и я не уверен, как это могло бы помочь с шаблонами... Кроме этого, я стараюсь избегать их, если ДЕЙСТВИТЕЛЬНО не требуется.
Еще один макрос foreach. T: тип, c: контейнер, i: итератор
#define foreach(T, c, i) for(T::iterator i=(c).begin(); i!=(c).end(); ++i)
#define foreach_const(T, c, i) for(T::const_iterator i=(c).begin(); i!=(c).end(); ++i)
Использование (показ концепции, не реально):
void MultiplyEveryElementInList(std::list<int>& ints, int mul)
{
foreach(std::list<int>, ints, i)
(*i) *= mul;
}
int GetSumOfList(const std::list<int>& ints)
{
int ret = 0;
foreach_const(std::list<int>, ints, i)
ret += *i;
return ret;
}
Доступны лучшие реализации: Google "BOOST_FOREACH"
Хорошие доступные статьи: Условная Любовь: FOREACH Redux (Эрик Ниблер) http://www.artima.com/cppsource/foreach.html
Вы можете #define
константы в командной строке компилятора с помощью -D
или же /D
вариант. Это часто полезно при кросс-компиляции одного и того же программного обеспечения для нескольких платформ, потому что вы можете сделать так, чтобы ваши make-файлы управляли тем, какие константы определены для каждой платформы.
Если у вас есть список полей, которые используются для множества вещей, например, для определения структуры, сериализации этой структуры в / из некоторого двоичного формата, выполнения вставок в базу данных и т. Д., То вы можете (рекурсивно!) Использовать препроцессор, чтобы избежать повторяя ваш список полей.
Это по общему признанию отвратительно. Но может быть, иногда лучше, чем обновлять длинный список полей в нескольких местах? Я использовал эту технику ровно один раз, и это было весьма полезно в этот раз.
Конечно, та же самая общая идея широко используется в языках с надлежащим отражением - просто изучите класс и оперируйте каждым полем по очереди. Выполнение этого в препроцессоре C хрупко, неразборчиво и не всегда переносимо. Поэтому я упоминаю об этом с некоторым трепетом. Тем не менее, вот оно...
(РЕДАКТИРОВАТЬ: теперь я вижу, что это похоже на то, что сказал @Andrew Johnson 9/18; однако идея рекурсивного включения того же файла продвигает идею немного дальше.)
// file foo.h, defines class Foo and various members on it without ever repeating the
// list of fields.
#if defined( FIELD_LIST )
// here's the actual list of fields in the class. If FIELD_LIST is defined, we're at
// the 3rd level of inclusion and somebody wants to actually use the field list. In order
// to do so, they will have defined the macros STRING and INT before including us.
STRING( fooString )
INT( barInt )
#else // defined( FIELD_LIST )
#if !defined(FOO_H)
#define FOO_H
#define DEFINE_STRUCT
// recursively include this same file to define class Foo
#include "foo.h"
#undef DEFINE_STRUCT
#define DEFINE_CLEAR
// recursively include this same file to define method Foo::clear
#include "foo.h"
#undef DEFINE_CLEAR
// etc ... many more interesting examples like serialization
#else // defined(FOO_H)
// from here on, we know that FOO_H was defined, in other words we're at the second level of
// recursive inclusion, and the file is being used to make some particular
// use of the field list, for example defining the class or a single method of it
#if defined( DEFINE_STRUCT )
#define STRING(a) std::string a;
#define INT(a) long a;
class Foo
{
public:
#define FIELD_LIST
// recursively include the same file (for the third time!) to get fields
// This is going to translate into:
// std::string fooString;
// int barInt;
#include "foo.h"
#endif
void clear();
};
#undef STRING
#undef INT
#endif // defined(DEFINE_STRUCT)
#if defined( DEFINE_ZERO )
#define STRING(a) a = "";
#define INT(a) a = 0;
#define FIELD_LIST
void Foo::clear()
{
// recursively include the same file (for the third time!) to get fields.
// This is going to translate into:
// fooString="";
// barInt=0;
#include "foo.h"
#undef STRING
#undef int
}
#endif // defined( DEFINE_ZERO )
// etc...
#endif // end else clause for defined( FOO_H )
#endif // end else clause for defined( FIELD_LIST )