Önişlemci makroları neden kötüdür ve alternatifler nelerdir?


94

Bunu her zaman sordum ama gerçekten iyi bir yanıt almadım; Sanırım ilk "Merhaba Dünya" yı yazmadan hemen hemen her programcı "makro asla kullanılmamalıdır", "makro kötüdür" gibi bir ifadeyle karşılaşmıştı, sorum şu: neden? Yeni C ++ 11 ile bunca yıldan sonra gerçek bir alternatif var mı?

Kolay kısım, #pragmaplatforma özgü ve derleyiciye özgü makrolar gibi makrolarla ilgilidir ve çoğu zaman bunun gibi ciddi kusurları #pragma oncevardır, en az 2 önemli durumda hataya açıktır: farklı yollarda ve bazı ağ kurulumlarında ve dosya sistemlerinde aynı isim.

Ama genel olarak, makrolar ve bunların kullanımına alternatifler ne olacak?


19
#pragmamakro değildir.
FooF

1
@foof önişlemci yönergesi?
user1849534

6
@ user1849534: Evet, işte bu ... ve makrolarla ilgili tavsiyeler hakkında konuşulmuyor #pragma.
Ben Voigt

1
Sen bir çok şey yapabilir constexpr, inlinefonksiyonlar ve templatesancak boost.preprocessorve chaosmakrolar yerlerini sahip olduğunu göstermiştir. Farklı derleyiciler, platformlar vb. İçin yapılandırma makrolarından bahsetmiyorum bile
Brandon

Yanıtlar:


165

Makrolar tıpkı diğer araçlar gibidir - cinayette kullanılan çekiç kötü değildir çünkü çekiçtir. Kişinin onu bu şekilde kullanması kötüdür. Çivi çakmak istiyorsanız, çekiç mükemmel bir araçtır.

Makroları "kötü" yapan birkaç yön vardır (her birini daha sonra genişleteceğim ve alternatifler önereceğim):

  1. Makrolarda hata ayıklayamazsınız.
  2. Makro genişleme garip yan etkilere neden olabilir.
  3. Makroların "ad alanı" yoktur, bu nedenle başka bir yerde kullanılan bir adla çakışan bir makronuz varsa, istemediğiniz yerde makro değiştirmeleri alırsınız ve bu genellikle garip hata mesajlarına yol açar.
  4. Makrolar, farkında olmadığınız şeyleri etkileyebilir.

Öyleyse burada biraz genişletelim:

1) Makrolarda hata ayıklanamaz. Bir sayıya veya dizeye çeviren bir makronuz olduğunda, kaynak kodda makro adı olur ve birçok hata ayıklayıcı, makronun neye dönüştüğünü "göremezsiniz". Yani gerçekte neler olduğunu bilmiyorsunuz.

Değiştirme : Kullanım enumveyaconst T

"İşlev benzeri" makrolar için, hata ayıklayıcı "bulunduğunuz her kaynak satırı" düzeyinde çalıştığından, makronuz bir ifade veya yüz olursa olsun tek bir ifade gibi hareket edecektir. Neler olduğunu anlamayı zorlaştırıyor.

Değiştirme : İşlevleri kullanın - "hızlı" olması gerekiyorsa satır içi (ancak çok fazla satır içi işlemin iyi bir şey olmadığına dikkat edin)

2) Makro genişletmelerin garip yan etkileri olabilir.

Ünlü olan #define SQUARE(x) ((x) * (x))ve kullanımıdır x2 = SQUARE(x++). Bu x2 = (x++) * (x++);, geçerli bir kod [1] olsa bile, programcının istediği neredeyse kesinlikle olmayacağı sonucuna götürür . Bir işlev olsaydı, x ++ yapmak iyi olurdu ve x yalnızca bir kez artar.

Başka bir örnek, makrolardaki "if else" dir, diyelim ki bizde:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

ve sonra

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

Aslında tamamen yanlış bir şey oluyor ...

Değiştirme : gerçek işlevler.

3) Makroların ad alanı yoktur

Bir makromuz varsa:

#define begin() x = 0

ve C ++ 'da begin kullanan bazı kodlarımız var:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

Şimdi, hangi hata mesajını aldığınızı düşünüyorsunuz ve bir hatayı nerede ararsınız [başka birinin yazdığı bir başlık dosyasında yaşayan başlangıç ​​makrosunu tamamen unuttuğunuzu - ya da bilmediğinizi varsayarsak -? [ve bu makroyu dahil etmeden önce eklerseniz daha da eğlencelidir - kodun kendisine baktığınızda kesinlikle hiçbir anlam ifade etmeyen garip hatalarda boğulursunuz.

Değiştirme : Bir "kural" kadar bir ikame yoktur - makrolar için yalnızca büyük harfli adlar kullanın ve diğer şeyler için hiçbir zaman büyük harfli adlar kullanmayın.

4) Makroların fark etmediğiniz etkileri vardır

Bu işlevi alın:

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ... 
void dostuff()
{
    int x = 7;

    begin();

    ... more code using x ... 

    printf("x=%d\n", x);

    end();

}

Şimdi, makroya bakmadan, startın x'i etkilememesi gereken bir fonksiyon olduğunu düşünürsünüz.

Bu tür şeyler ve çok daha karmaşık örnekler gördüm, GERÇEKTEN gününüzü alt üst edebilir!

Değiştirme : x'i ayarlamak için bir makro kullanmayın veya x'i bir bağımsız değişken olarak iletin.

Makro kullanmanın kesinlikle faydalı olduğu zamanlar vardır. Bir örnek, dosya / satır bilgilerini iletmek için bir işlevi makrolarla sarmalamaktır:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Artık my_debug_mallockodda normal malloc olarak kullanabiliriz , ancak fazladan argümanları var, bu yüzden sona gelince ve "hangi bellek öğelerinin serbest bırakılmadığını" taradığımızda, tahsisin nerede yapıldığını yazdırabiliriz, böylece programcı sızıntıyı takip edebilir.

[1] Bir değişkeni birden fazla "sıra noktasında" güncellemek tanımsız bir davranış. Bir sıra noktası, bir ifade ile tam olarak aynı değildir, ancak çoğu niyet ve amaç için, onu bu şekilde değerlendirmeliyiz. Bunu yapmak iki kez x++ * x++güncellenecektir x, bu tanımsızdır ve muhtemelen farklı sistemlerde farklı değerlere ve farklı sonuç değerlerine yol açacaktır x.


6
if elseSorunlar makro vücut içini saran çözülebilir do { ... } while(0). Bu, ifve forve diğer potansiyel olarak riskli kontrol akışı sorunları açısından bekleneceği şekilde davranır . Ama evet, gerçek bir işlev genellikle daha iyi bir çözümdür. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Aaron McDaid

11
@AaronMcDaid: Evet, bu makrolarda açığa çıkan bazı sorunları çözen bazı geçici çözümler var. Yazımın amacı makroların nasıl iyi yapılacağını göstermek değil, iyi bir alternatifin olduğu "makroları yanlış yapmanın ne kadar kolay olduğunu" göstermekti. Bununla birlikte, makroların çok kolay çözdüğü şeyler vardır ve makroların da yapılması gereken doğru şey olduğu zamanlar vardır.
Mats Petersson

1
3. noktada, hatalar artık gerçekten bir sorun değil. Clang gibi modern derleyiciler benzer bir şey söyleyecek note: expanded from macro 'begin've nerede begintanımlandığını gösterecektir .
kirbyfan64sos

5
Makroların diğer dillere çevrilmesi zordur.
Marco van de Voort

1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (bu cevapta biraz aşağı, i ++ * i ++ ve benzeri hakkında konuşuyor.
Mats Petersson

21

"Makrolar kötüdür" ifadesi genellikle #pragma değil, #define kullanımına atıfta bulunur.

Özellikle ifade şu iki duruma atıfta bulunur:

  • sihirli sayıları makro olarak tanımlama

  • ifadeleri değiştirmek için makro kullanma

yeni C ++ 11 ile bunca yıldan sonra gerçek bir alternatif var mı?

Evet, yukarıdaki listedeki öğeler için (sihirli sayılar const / constexpr ile tanımlanmalı ve ifadeler [normal / satır içi / şablon / satır içi şablon] işlevleriyle tanımlanmalıdır.

Sihirli sayıları makrolar olarak tanımlayarak ve ifadeleri makrolarla değiştirerek (bu ifadeleri değerlendirmek için işlevler tanımlamak yerine) ortaya çıkan sorunlardan bazıları şunlardır:

  • sihirli sayılar için makroları tanımlarken, derleyici tanımlanan değerler için tür bilgisi tutmaz. Bu, derleme uyarılarına (ve hatalara) neden olabilir ve kodda hata ayıklayan kişilerin kafasını karıştırabilir.

  • fonksiyonlar yerine makroları tanımlarken, bu kodu kullanan programcılar, fonksiyonlar gibi çalışmalarını beklerler ve yapmazlar.

Bu kodu düşünün:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

C'ye atamadan sonra a ve c'nin 6 olmasını beklersiniz (makro yerine std :: max kullanarak olacağı gibi). Bunun yerine, kod şunları gerçekleştirir:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

Bunun da ötesinde, makrolar ad alanlarını desteklemez; bu, kodunuzda makroların tanımlanması, istemci kodunu kullanabilecekleri adlarla sınırlayacağı anlamına gelir.

Bu, yukarıdaki makroyu (maks. İçin) tanımlarsanız, #include <algorithm>açıkça yazmadığınız sürece aşağıdaki kodların hiçbirinde artık yapamayacağınız anlamına gelir :

#ifdef max
#undef max
#endif
#include <algorithm>

Değişkenler / işlevler yerine makrolara sahip olmak, adreslerini alamayacağınız anlamına da gelir:

  • sabit olarak bir makro sihirli bir sayı olarak değerlendirilirse, onu adresle geçiremezsiniz

  • bir makro-işlev için, onu bir yüklem olarak kullanamaz veya işlevin adresini alamaz veya onu bir işlev olarak ele alamazsınız.

Düzenleme: Örnek olarak, #define maxyukarıdakilere doğru alternatif :

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

Bu, makronun yaptığı her şeyi tek bir sınırlama ile yapar: bağımsız değişkenlerin türleri farklıysa, şablon sürümü sizi açık olmaya zorlar (bu aslında daha güvenli, daha açık koda yol açar):

int a = 0;
double b = 1.;
max(a, b);

Bu max bir makro olarak tanımlanırsa, kod derlenir (bir uyarı ile).

Bu maksimum bir şablon işlevi olarak tanımlanırsa, derleyici belirsizliği belirtir ve ya max<int>(a, b)da demeniz gerekir max<double>(a, b)(ve böylece açıkça niyetinizi belirtirsiniz).


1
C ++ 11'e özgü olması gerekmez; makroları ifade olarak kullanmak için işlevleri ve sabit olarak makro kullanımını değiştirmek için [statik] const / constexpr'i değiştirebilirsiniz.
utnapistim

1
C99 bile kullanımına izin verir ve const int someconstant = 437;bir makronun kullanılacağı hemen hemen her şekilde kullanılabilir. Aynı şekilde küçük işlevler için. C'de normal ifadede çalışmayacak bir makro olarak bir şeyler yazabileceğiniz birkaç şey vardır (C'nin yapamayacağı herhangi bir sayı türünün ortalamasını alan bir şey yapabilirsiniz - ancak C ++ 'da şablonlar vardır bunun için). C ++ 11, "bunun için makrolara ihtiyacınız olmayan" birkaç şey daha eklerken, çoğunlukla daha önceki C / C ++ 'da çözülmüştür.
Mats Petersson

Bir argüman iletirken ön artış yapmak korkunç bir kodlama uygulamasıdır. Ve C / C ++ 'da kodlayan hiç kimse , işlev benzeri bir çağrının makro olmadığını varsaymamalıdır.
StephenG

Pek çok uygulama, tanımlayıcıları gönüllü olarak parantez içine alır maxve minbunların ardından sol parantez gelir. Ancak bu tür makroları tanımlamamalısınız ...
LF

14

Yaygın bir sorun şudur:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Önişlemci bunu şu şekilde genişleteceği için 5 değil 10 yazdıracak:

printf("25 / (3+2) = %d", 25 / 3 + 2);

Bu sürüm daha güvenlidir:

#define DIV(a,b) (a) / (b)

2
ilginç bir örnek, temelde sadece anlambilim içermeyen simgelerdir
user1849534

Evet. Makroya verildikleri şekilde genişletilirler. DIVMakro () civarında olan bir çift yeniden yazılabilir b.
phaazon

2
Yani #define DIV(a,b)değil #define DIV (a,b), ki bu çok farklı.
rici

6
#define DIV(a,b) (a) / (b)yeterince iyi değil; genel bir uygulama olarak, her zaman en dıştaki parantezleri şu şekilde ekleyin:#define DIV(a,b) ( (a) / (b) )
PJTraill

3

Makrolar, özellikle genel kod (makronun parametreleri herhangi bir şey olabilir), bazen parametrelerle oluşturmak için değerlidir.

Dahası, bu kod makronun kullanıldığı noktaya yerleştirilir (yani eklenir).

OTOH, aşağıdakilerle benzer sonuçlar elde edilebilir:

  • aşırı yüklenmiş fonksiyonlar (farklı parametre türleri)

  • şablonlar, C ++ 'da (genel parametre türleri ve değerleri)

  • satır içi işlevler (tek noktalı bir tanıma atlamak yerine çağrıldıkları yer kodu - ancak, bu daha çok derleyici için bir öneridir).

düzenleme: makronun neden kötü olduğuna gelince:

1) argümanların tür kontrolü yok (türleri yoktur), bu nedenle kolayca kötüye kullanılabilir 2) bazen çok karmaşık koda genişler, bu önceden işlenmiş dosyada tanımlanması ve anlaşılması zor olabilir 3) hata yapmak kolaydır - makrolarda eğilimli kod, örneğin:

#define MULTIPLY(a,b) a*b

ve sonra ara

MULTIPLY(2+3,4+5)

genişleyen

2 + 3 * 4 + 5 (ve içine değil: (2 + 3) * (4 + 5)).

İkincisine sahip olmak için şunları tanımlamalısınız:

#define MULTIPLY(a,b) ((a)*(b))

3

Önişlemci tanımlarını veya makroları sizin deyiminizle kullanmanın yanlış olduğunu düşünmüyorum.

Onlar c / c ++ 'da bulunan bir (meta) dil konseptidir ve diğer araçlar gibi, ne yaptığınızı biliyorsanız hayatınızı kolaylaştırabilirler. Makrolarla ilgili sorun, c / c ++ kodunuzdan önce işlenmeleri ve hatalı olabilecek ve tamamen açık olan derleyici hatalarına neden olabilecek yeni kod üretmeleridir. İşin iyi yanı, kodunuzu temiz tutmanıza ve doğru kullanıldığında çok fazla yazmadan tasarruf etmenize yardımcı olabilirler, bu nedenle kişisel tercihinize gelir.


Ayrıca, diğer yanıtların da işaret ettiği gibi, kötü tasarlanmış önişlemci tanımları, geçerli sözdizimine sahip kod üretebilir, ancak farklı anlamsal anlamlara sahiptir; bu, derleyicinin şikayet etmeyeceği ve kodunuzda bulunması daha da zor olacak bir hata oluşturduğunuz anlamına gelir.
Sandi Hrvić

3

C / C ++ 'daki makrolar, sürüm kontrolü için önemli bir araç görevi görebilir. Aynı kod, küçük bir Makro yapılandırmasıyla iki istemciye teslim edilebilir. Gibi şeyler kullanıyorum

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

Bu tür bir işlevsellik, makrolar olmadan o kadar kolay mümkün değildir. Makrolar aslında harika bir Yazılım Yapılandırma Yönetim Aracıdır ve yalnızca kodun yeniden kullanımı için kısayollar oluşturmanın bir yolu değildir. Makrolarda yeniden kullanılabilirlik amacıyla fonksiyonların tanımlanması kesinlikle sorun yaratabilir.


Derleme sırasında bir kod tabanından iki değişken oluşturmak için makro değerlerini cmdline üzerinde ayarlamak gerçekten güzel. aşırıya kaçmadan.
kevinf

1
Bir bakış açısıyla, bu kullanım en tehlikeli olanıdır: araçlar (IDE'ler, statik çözümleyiciler, yeniden düzenleme) olası kod yollarını bulmakta zorlanacaktır.
erenon

1

Bence problem, makroların derleyici tarafından iyi optimize edilmemesi ve okunması ve hata ayıklaması için "çirkin" olmaları.

Genellikle iyi bir alternatif, genel işlevler ve / veya satır içi işlevlerdir.


2
Makroların iyi optimize edilmediğine inanmanıza neden olan nedir? Basit metin ikameleridir ve sonuç, makrolar olmadan yazılan kod kadar optimize edilmiştir.
Ben Voigt

@BenVoigt ama semantiği dikkate almıyorlar ve bu "optimal olmayan" olarak değerlendirilebilecek bir şeye yol açabilir ... en azından bu stackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534: Derleme bağlamında "optimize edilmiş" kelimesinin anlamı bu değildir.
Ben Voigt

1
@BenVoigt Kesinlikle, makrolar yalnızca metin ikamesidir. Derleyici sadece kodu kopyalar, bu bir performans sorunu değildir, ancak program boyutunu artırabilir. Özellikle program boyutu sınırlamalarına sahip olduğunuz bazı bağlamlarda geçerlidir. Bazı kodlar makrolarla o kadar dolu ki programın boyutu iki katına çıkıyor.
Davide Icardi

1

Önişlemci makroları, aşağıdaki gibi amaçlanan amaçlar için kullanıldıklarında kötü değildir:

  • #İfdef türü yapılar kullanarak aynı yazılımın farklı sürümlerini oluşturma, örneğin farklı bölgeler için pencerelerin yayınlanması.
  • Kod testi ile ilgili değerleri tanımlamak için.

Alternatifler - Benzer amaçlar için ini, xml, json formatında bir takım konfigürasyon dosyaları kullanılabilir. Ancak bunları kullanmak, bir önişlemci makrosunun önleyebileceği kod üzerinde çalışma zamanı etkilerine sahip olacaktır.


1
çünkü C ++ 17 constexpr if + "config" constexpr değişkenlerini içeren bir başlık dosyası # ifdef'lerin yerini alabilir.
Enhex
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.