D Olanaklarının C Önişlemcisiyle [preprocessor] Karşılaştırılması
Bu sayfadaki bilgiler Digital Mars'ın sitesindeki aslından alınmıştır.
C'nin yaratıldığı günlerde derleyici teknolojisi henüz çok geriydi. Derleme işleminin öncesine metin makroları işleyen bir adım eklemek, çok kolay bir ek olması yanında programcıya büyük kolaylıklar da sunmuştu. Fakat programların boyutları ve karmaşıklıkları arttıkça önişlemci olanaklarının üstesinden gelinemeyecek sorunları da ortaya çıkmaya başlamıştır. D'de önişlemci bulunmaz, ama başka olanakları sayesinde aynı sorunları daha etkin olarak çözer.
- Başlık dosyaları
- #pragma once
- #pragma pack
- Makrolar
- Derlemeyi duruma göre yönlendirmek [conditional compilation]
- Kod tekrarını azaltmak
#error
ve derleme zamanı denetimleri- Şablon katmaları [mixin]
Başlık dosyaları
C
Başlık dosyalarını oldukları gibi metin halinde #include
etmek, C ve C++'da çok gündelik işlemlerdir. Bu yüzden derleyici başlık dosyalarında bulunan on binlerce satırı her seferinde baştan derlemek zorunda kalır. Başlık dosyalarının asıl yapmaya çalıştıkları; metin olarak eklenmek yerine, derleyiciye bazı isimleri bildirmektir. Bu D'de import
deyimiyle yapılır. Bu durumda daha önceden zaten derlenmiş olan isimler öğrenilmiş olurlar. Böylelikle başlıkların birden fazla sayıda eklenmelerini önlemek için #ifdef
'ler, #pragma once
'lar, veya kullanımları çok kırılgan olan precompiled headers gibi çözümler gerekmemiş olur.
#include <stdio.h>
D
import std.c.stdio;
#pragma once
C
Başlık dosyalarının birden fazla #include
edilmelerini önlemek gerekir. Bunun için başlık dosyalarına ya şu satır eklenir:
#pragma once
ya da daha taşınabilir olarak:
#ifndef __STDIO_INCLUDE #define __STDIO_INCLUDE ... dosya içeriği #endif
D
Tamamen gereksizdir; çünkü D, isimleri sembol olarak ekler. import
bildirimi ne kadar çok yapılmış olursa olsun, isimler yalnızca bir kere eklenirler.
#pragma pack
C
Yapı üyelerinin bellekte nasıl yerleştirileceklerini belirler.
D
D sınıflarında üyelerin bellek yerleşimlerine karışmak gerekmez; derleyici üyelerin ve hatta yerel değişkenlerin sıralarını istediği gibi değiştirme hakkına sahiptir; ama dış veri yapılarına denk olmaları gerekebileceği için, D'de yapı üyelerinin yerleşimleri de ayarlanabilir:
struct Foo { align (4): // 4 baytlık yerleşim kullanılır ... }
Makrolar
C'de önişlemci makroları esneklik de sağlayan çok güçlü olanaklardır; ama zayıflıkları da vardır:
- Makroların kapsamları yoktur; tanımlandıkları noktadan kaynak dosyanın sonuna kadar etkilidirler. Etkileri .h dosyalarının aşar, ve içteki kapsamlara da geçer.
#include
edilmiş olan on binlerce satırda içinde tanımlanmış olan makroların etkileri bazen istenmeyen sonuçlar doğurur. - Hata ayıklayıcının makrolardan haberi yoktur. Hata ayıklayıcının gördüğü kod, makroların açılmış halleri olduğu için; programcının gördüğü ile aynı değildir.
- Makrolar simgelerin ayrıştırılmalarını olanaksızlaştırabilirler; çünkü daha önce tanımlanmış bir makro, kodu bütünüyle değiştirmiş olabilir.
- Makroların bütünüyle metin temelli olmaları kullanımlarının tutarsız olmalarına ve dolayısıyla hataya açık olmalarına yol açar. (C++ şablonları bir ölçüde bunları önler.)
- Makroların görevlerinden birisi dildeki eksiklikleri gidermektir. Bunu başlık dosyalarını sarmalayan
#ifdef/#endif
satırlarında görüyoruz.
Makroların temel kullanımları ve bunların D'deki karşılıkları şöyledir:
- Sabit değerler
C
#define DEGER 5
D
const int DEĞER = 5;
- Bir dizi sabit değer veya bayrak değerleri tanımlamak
C
int bayraklar: #define BAYRAK_X 0x1 #define BAYRAK_Y 0x2 #define BAYRAK_Z 0x4 ... bayraklar |= BAYRAK_X;
D
enum BAYRAK { X = 0x1, Y = 0x2, Z = 0x4 }; BAYRAK bayraklar; ... bayraklar |= BAYRAK.X;
- ASCII (char) veya evrensel (wchar) karakterlere göre derlemek
C
#if UNICODE #define dchar wchar_t #define TEXT(s) L##s #else #define dchar char #define TEXT(s) s #endif ... dchar m[] = TEXT("merhaba");
D
dchar[] m = "merhaba";
- Eski derleyicilere uygun olarak derleme
C
#if PROTOTYPES #define P(p) p #else #define P(p) () #endif int func P((int x, int y));
D
D derleyicisinin açık kodlu olması farklı derleyici söz dizimleri sorunlarını da önler.
- Türlere yeni isimler
C
#define INT int
D
alias int INT;
- Bildirimler ve tanımlar için tek başlık dosyası kullanmak
C
#define EXTERN extern #include "bildirimler.h" #undef EXTERN #define EXTERN #include "bildirimler.h"
bildirimler.h
dosyasında:EXTERN int foo;
D
D'de bildirim ve tanım aynıdır; aynı kaynak koddan hem bildirim hem de tanım elde etmek için
extern
anahtar sözcüğünü kullanmak gerekmez. - Hızlı iç [inline] fonksiyonlar
C
#define X(i) ((i) = (i) / 3)
D
int X(ref int i) { return i = i / 3; }
Derleyici eniyileştiricisi fonksiyonu zaten iç fonksiyon olarak derler.
assert
fonksiyonu için dosya ve satır bilgisiC
#define assert(e) ((e) || _assert(__LINE__, __FILE__))
D
assert
dilin bir iç olanağıdır. Bu, derleyici eniyileştiricisinin_assert()
'in dönüş türünün olmadığı bilgisinden yararlanmasını da sağlar.- Fonksiyon çağırma çeşitleri [calling conventions]
C
#ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 fonksiyon();
D
Fonksiyonların nasıl çağrıldıkları gruplar halinde belirlenebilir:
extern (Windows) { int bir_fonksiyon(); int baska_bir_fonksiyon(); }
__near
ve__far
işaretçi yazımını gizlemekC
#define LPSTR char FAR *
D
D'de 16 bitlik kod, işaretçi boyutlarının karışık kullanımı, değişik işaretçi türleri desteklenmez.
- Türden bağımsız temel kodlama
C
Hangi fonksiyonun kullanılacağını metni dönüştürerek belirlemek:
#ifdef UNICODE int getValueW(wchar_t *p); #define getValue getValueW #else int getValueA(char *p); #define getValue getValueA #endif
D
D isimlere yeni takma isimler vermeyi destekler:
version (UNICODE) { int getValueW(wchar[] p); alias getValueW getValue; } else { int getValueA(char[] p); alias getValueA getValue; }
Derlemeyi duruma göre yönlendirmek [conditional compilation]
C
Derlemeyi yönlendirebilmek önişlemcinin güçlü yönlerindendir ama zayıflıkları da vardır:
- Önişlemcinin kapsamlardan haberi olmaması,
#if/#endif
bloklarının kapsamlardan bağımsız ve düzensiz olarak yerleştirilmelerine olanak sağlar; kodu izlemek güçleşir. - Derlemeyi yönlendirmek yine makrolar aracılığıyla yapıldığı için, kullanılan makro isimlerinin programdaki başka isimlerle çakışma olasılıkları yine de geçerlidir.
#if
ifadeleri diğer C ifadelerinden farklı olarak işletilirler.- Önişlemci dili C'den temelde farklılıklar gösterir; örneğin boşluk karakterleri ve satır başları önişlemci için önemli oldukları halde derleyici için önemli olmayabilirler.
D
Derlemeyi yönlendirmek D'de dil tarafından sağlanır:
- Farklı sürüm kodlarını farklı modüllere yerleştirmek
- Hata ayıklamaya yardımcı olan
debug
deyimi - Tek kaynak koddan programın farklı sürümlerini üretmeye yarayan
version
deyimi if(0)
deyimi- Bir grup kod satırını bir grup açıklama satırına dönüştüren
/+ +/
açıklamaları
Kod tekrarını azaltmak
C
Bazen fonksiyonlar içinde tekrarlanan kodlarla karşılaşılabilir. Tekrarlanan kodların ayrı bir fonksiyona taşınması daha doğal olsa da, fonksiyon çağrısı bedelini önlemek için bazen makrolar kullanılır. Örneğin şu bayt kod yorumlayıcısı koduna bakalım:
unsigned char *ip; // byte code instruction pointer int *stack; int spi; // stack pointer ... #define pop() (stack[--spi]) #define push(i) (stack[spi++] = (i)) while (1) { switch (*ip++) { case ADD: op1 = pop(); op2 = pop(); result = op1 + op2; push(result); break; case SUB: ... } }
Bu kodda çeşitli sorunlar vardır:
- Makrolar ifade olarak açıldıkları için değişken tanımlayamazlar. Örneğin program yığıtından taşmaya karşı denetim eklemek gibi bir iş oldukça güçtür.
- Sembol tablosunda bulunmadıklarından, etkileri tanımlandıkları fonksiyonun dışına da taşar
- Makro parametreleri metin olarak geçirildiklerinden makronun parametreyi gereğinden fazla işletmemeye dikkat etmesi ve kodun anlamını korumak için parametreyi parantezler içine alması gerekir
- Hata ayıklayıcı makrolardan habersizdir
D
D'de kapsam fonksiyonları kullanılır:
ubyte* ip; // byte code instruction pointer int[] stack; // operand stack int spi; // stack pointer ... int pop() { return stack[--spi]; } void push(int i) { stack[spi++] = i; } while (1) { switch (*ip++) { case ADD: op1 = pop(); op2 = pop(); push(op1 + op2); break; case SUB: ... } }
Bunun çözdüğü sorunlar şunlardır:
- Kapsam fonksiyonları D fonksiyonları kadar güçlü olanaklara sahiptirler
- Kapsam fonksiyonunun ismi, tanımlandığı kapsamla sınırlıdır
- Parametreler değer olarak geçirildikleri için birden fazla işletilmeleri gibi sorunlar yoktur
- Kapsam fonksiyonları hata ayıklayıcı tarafından görülebilirler
Ek olarak, kapsam fonksiyonu çağrıları eniyileştirici tarafından yok edilince çağrı bedeli de önlenmiş olur.
#error
ve derleme zamanı denetimleri
Derleme zamanında yapılan denetimler sırasında derlemenin bir hata mesajıyla sonlandırılması sağlanabilir.
C
Bir yöntem, #error
önişlemci komutunu kullanmaktır:
#if FOO || BAR ... derlenecek kod ... #else #error "FOO veya BAR'dan en az birisi gerekmektedir" #endif
Önişlemci yetersizlikleri burada da geçerlidir: yalnızca tamsayı ifadeler geçerlidir, tür değişimlerine izin verilmez, sizeof
kullanılamaz, sabitler kullanılamaz, vs.
Bunların bazılarını gidermek için bir static_assert
(derleme zamanı denetimi) makrosu kullanılabilir (M.Wilson tarafından):
#define static_assert(_x) \ do { \ typedef int ai[(_x) ? 1 : 0]; \ } while(0)
Kullanımı şöyledir:
void foo(T t) { static_assert(sizeof(T) < 4); ... }
Bu, koşul yanlış (false
) olduğunda bir yazım hatasına dönüşür ve derleme bir hata mesajıyla sonlanır. [Çevirenin notu: false
olduğunda ai
dizisinin boyu 0
olur; bu da C kurallarına göre yasal değildir.]
D
static assert
D'de bir dil olanağıdır ve bildirimlerin ve deyimlerin kullanılabildikleri her yerde kullanılabilir. Örnek:
version (FOO) { class Bar { const int x = 5; static assert(Bar.x == 5 || Bar.x == 6); void foo(T t) { static assert(T.sizeof < 4); ... } } } else version (BAR) { ... } else { static assert(0); // yasal olmayan bir sürüm }
Şablon katmaları [mixin]
D'nin şablon katma olanağı, dışarıdan bakıldığında tıpkı C'nin önişlemcisinde olduğu gibi bir metin yerleştirme olanağına benzer. Yerleştirilen metin yine açıldığı yerde ayrıştırılır [parse], ama katmaların makrolardan üstün tarafları vardır:
- Katmalar söz dizimine uyarlar ve ayrıştırma ağaçlarında [parse tree] yer alırlar. Makrolar ise hiçbir düzene bağlı olmadan metin yerleştirirler.
- Katmalar aynı dilin parçalarıdırlar. Makrolar ise C++'nın üstünde ayrı bir dildir; kendine ait ifade kuralları, türleri, sembol tabloları, ve kapsam kuralları vardır.
- Katmalar kısmi özelleme [partial specialization] kurallarına göre seçilirler; makrolarda aşırı yükleme yoktur.
- Katmalar kapsam oluştururlar; makrolar oluşturmazlar.
- Katmalar söz diziminden anlayan araç programlarla uyumludurlar; makrolar değildirler.
- Katmalarla ilgili bilgiler ve sembol tabloları hata ayıklayıcılara geçirilirler; makrolar metin yerleştirildikten sonra yok olurlar.
- Katmalarda birden fazla tanımın uygun olduğu durumlarda hangisinin kullanılacağının kuralları bellidir; makro tanımları çakışmak zorundadırlar.
- Katma isimlerinin başka katma isimleriyle çakışmaları standart bir algoritma yoluyla önlenmiştir; makrolarda kelime birleştirme yoluyla elle yapılır.
- Katma argümanları tek bir kere işletilirler; makro açılımlarında ise argümanlar kullanıldıkları her sefer işletilirler ve bu yüzden bulunması güç hatalara yol açabilirler.
- Katma argümanlarının etrafına koruyucu parantezler koymak gerekmez.
- Katmalar istendiği kadar uzunlukta normal D kodu olarak yazılırlar; makrolarda olduğu gibi satırları
\
karakteriyle sonlandırmak gerekmez,//
açıklamaları koyamamak gibi sorunlar yoktur, vs. - Katmalar başka katmalar tanımlayabilirler; makrolar makro tanımlayamazlar.