Ja hobbystycznie czasem programuję w C na mikrokontrolery i C++/Qt na Windows. W obu przypadkach często korzystam z dyrektywy #define i czasem z #ifdef. U mnie najczęstsze zastosowania są następujące:
- Włączanie i wyłączanie fragmentów kodu poprzez zmianę w jednym miejscu (na przykład, jak testuję różne rozwiązania tego samego, bądź jak chce mieć możliwość wydruku pewnych informacji przez cout tylko do testów, podczas, gdy ostatecznie ten wydruk nie jest potrzebny).
- Stała, która pojawia się w wielu miejscach w kodzie, również w definicji zmiennej (np. wielkość tablicy).
- Bardziej skomplikowane wyrażenie, zamiast pisać w wielu miejscach to robię #define i w nim to wyrażenie, a jak potrzebuję zmienić, to zmieniam w jednym miejscu.
Ja doskonale znam sposób działania #define i używam szczególnie, jak zależy mi na wydajności (kod funkcji jest bardzo często uruchamiany w pętli). Wiem co to jest "inline", ale czytałem, że nie każdy kompilator i nie w każdym przypadku to respektuje. A jak chce się używać tego samego dla zmiennych różnych typów, to albo musi być tyle funkcji, ile jest możliwości, albo kompilator musi dodać zmianę typu zmiennej przy uruchamianiu funkcji, podczas, gdy to nie jest potrzebne. Wyrażenia warunkowe dla wartości stałych? Trochę bez sensu obsługiwać zmienną, która przez cały czas będzie mieć tą samą wartość i na przykład w zależności od niej ma uruchomić lub pominąć jakiś kod. A jak wyrażenie nie jest nigdzie użyte, to nie powstaje martwy kod.
Widzę, że w innych językach nie ma preprocesora, w C# jakiś jest, ale nie ma możliwości zdefiniowania stałych fragmentów kodu (czyli #define z wyrażeniem, które ma być wstawiane w miejscu zdefiniowanego słowa).
Przykładowo, mam takie wyrażenia, które używam w swoich projektach. jest to chyba lepsze rozwiązanie wydajnościowo niż robienie funkcji dla różnych typów liczb, prawda?
#define Diff(X, Y) ((((X) - (Y)) > 0) ? ((X) - (Y)) : ((Y) - (X))) // Różnica bezwzględna
#define Abs(X) (((X) > 0) ? (X) : (0 - (X))) // Wartość bezwzględna
#define Min(X, Y) (((X) > (Y)) ? (Y) : (X)) // Wartość minimalna z dwóch
#define Max(X, Y) (((X) < (Y)) ? (Y) : (X)) // Wartość maksymalna z dwóch
#define Shl(X, Y) (((Y) >= 0) ? ((X) << (Y)) : ((X) >> (0 - (Y)))) // Przesunięcie bitowe w lewo, ale wartość ujemna w ilości bitów powoduje przesunięcie w prawo
#define Shr(X, Y) (((Y) >= 0) ? ((X) >> (Y)) : ((X) << (0 - (Y)))) // Przesunięcie bitowe w prawo, ale wartość ujemna w ilości bitów powoduje przesunięcie w lewo
#define Range(X, A, B) (((X) < (A)) ? (A) : (((X) > (B)) ? (B) : (X))) // Ograniczenie wartości do wskazanego zakresu, jak wykracza poza zakres, to wartość jest zmieniana do wartości granicznej zakresu
Czy może #define ma jakieś wady, które sprawiają, że w innych językach programowania nie ma takiej możliwości?
Czy w "poważnych" (dużych, komercyjnych) projektach w C i C++ używa się #define? Jeżeli nie to dlaczego (chodzi o inny powód niż brak potrzeby w danym projekcie)?
Inny przykład, który wymyśliłem na własne potrzeby, który pozwala wychwycić utworzone i niezniszczone obiekty (wyciek pamięci), ponowne zniszczenie zniszczonego obiektu, pomylenie delete z delete[], pomieszanie new/delete z malloc/free:
#define OBJMEMSTR std::cout
#define OBJMEMSIZETYPE size_t
#ifdef OBJMEMDEBUG
#define NEW(T, C) ([](T * __ptr) -> T* { OBJMEMSTR << "NEW_OBJ_" << (void*)__ptr << "_" << #T << std::endl; return __ptr; })(new C)
#define NEWARR(T, C) ([](T * __ptr) -> T* { OBJMEMSTR << "NEW_ARR_" << (void*)__ptr << "_" << #T << std::endl; return __ptr; })(new C)
#define NEWARRINIT(T, C, N, V) ([](T * __ptr, OBJMEMSIZETYPE __N, T __V) -> T* { OBJMEMSTR << "NEW_ARR_" << (void*)__ptr << "_" << #T << std::endl; for (OBJMEMSIZETYPE __it = 0; __it < __N; __it++) { __ptr[__it] = __V; } return __ptr; })(new C, N, V)
#define DEL(T, P) ([](T * __ptr) -> void { OBJMEMSTR << "DEL_OBJ_" << (void*)__ptr << "_" << #T << std::endl; delete __ptr; })(P)
#define DELARR(T, P) ([](T * __ptr) -> void { OBJMEMSTR << "DEL_ARR_" << (void*)__ptr << "_" << #T << std::endl; delete[] __ptr; })(P)
#define MALLOC(S) ([](OBJMEMSIZETYPE __S) -> void* { void * __ptr = malloc(__S); OBJMEMSTR << "NEW_MEM_" << __ptr << "_void" << std::endl; return __ptr; })(S)
#define CALLOC(N, S) ([](OBJMEMSIZETYPE __N, OBJMEMSIZETYPE __S) -> void* { void * __ptr = calloc(__N, __S); OBJMEMSTR << "NEW_MEM_" << __ptr << "_void" << std::endl; return __ptr; })(N, S)
#define FREE(P) ([](void * __ptr) -> void { OBJMEMSTR << "DEL_MEM_" << __ptr << "_void" << std::endl; free(__ptr); })(P)
#define REALLOC(P, S) ([](void * __ptr, OBJMEMSIZETYPE __S) -> void* { void * __ptr0; OBJMEMSTR << "DEL_MEM_" << __ptr << "_void" << std::endl; __ptr0 = realloc(__ptr, __S); OBJMEMSTR << "NEW_MEM_" << __ptr0 << "_void" << std::endl; return __ptr0; })(P, S)
#else
#define NEW(T, C) new C
#define NEWARR(T, C) new C
#define NEWARRINIT(T, C, N, V) ([](T * __ptr, OBJMEMSIZETYPE __N, T __V) -> T* { for (OBJMEMSIZETYPE __it = 0; __it < __N; __it++) { __ptr[__it] = __V; } return __ptr; })(new C, N, V)
#define DEL(T, P) delete P
#define DELARR(T, P) delete[] P
#define MALLOC(S) malloc(S)
#define CALLOC(N, S) calloc(N, S)
#define FREE(P) free(P)
#define REALLOC(P, S) realloc(P, S)
#endif
Wystarczy zamiast new i delete używać NEW i DELETE, a po przetestowaniu programu skasować #define OBJMEMDEBUG i program skompiluje się dokładnie tak, jakby w ogóle tego powyższego nie było.