C Dilinin Yapması Gereken Apaçık Şeyler
(digitalmars.com)- C23’e kadar standart C değişmeye devam etti, ancak Walter Bright sabit ifade değerlendirmesi, ileriye başvuru ve header bağımlılığı gibi eski rahatsızlıkların hâlâ geliştirme akışını engellediğini düşünüyor
- D derleyicisine yerleşik C derleyicisi ImportC, yeni bir uygulama olmanın avantajını kullanarak
constant-expressionkonumlarında koşulları karşılayan fonksiyonları derleme zamanında çalıştırabiliyor - CTFE mümkün olduğunda
_Static_assert(sum(3, 4) == 7,...)gibi birim testleri, ayrı bir çalıştırılabilir dosya olmadan her derlemede çalıştırılabiliyor - Standart C’deki bildirim sırası kısıtı, ileri bildirim tekrarına ve kodun ters sırada yerleştirilmesine yol açıyor; ImportC ise global bildirim sırasına daha az bağlı bir yaklaşım benimsiyor
__import dex;gibi.cdosyasındaki bildirimleri doğrudan içe aktarmak, ayrı.hdosyalarını azaltıyor ve header ile uygulama arasındaki uyumsuzluklardan doğan hata ayıklama yükünü de düşürüyor
Standart C’de Kalan Rahatsızlıklar
- Standart C, C23’e kadar düzenli olarak iyileştirildi; ancak Walter Bright hâlâ düzeltilmemiş kısımlar kaldığını düşünüyor
- Dlang topluluğu, D programlama dili derleyicisinin içine bir C derleyicisi yerleştirdi ve bu derleyiciye ImportC adını veriyor
- ImportC, baştan yeni yazıldığı için modern derleyici teknolojileriyle C’nin eksik yönlerini ele alma alanına sahipti
- Yazıda ele alınan başlıklar dört tane
constant-expressiondeğerlendirmesi- Derleme zamanında birim test
- Bildirimlerde ileriye başvuru
- Bildirimleri içe aktarma
Derleme Zamanında Fonksiyon Çalıştırma ve Sabit İfade Değerlendirmesi
- C, basit ifadeleri sabit katlama (constant folding) ile derleme zamanında hesaplayabilir; ancak standart C’de fonksiyonlar derleme zamanında çalıştırılamaz
- Örnek kod
enum E { A = 3, B = 4, C = sum(5, 6) };gcc ile derlendiğinde,Cnumaralandırıcısının değerinin tamsayı sabiti olmadığına dair bir hata oluşur - ImportC aynı kodu derleyebilir
- Yaklaşım, C sözdiziminde
constant-expressiongörülen her yerde derleyicinin fonksiyonları derleme zamanında çalıştırabilmesi gerektiği yönünde - Ancak ilgili fonksiyon I/O, değiştirilebilir global değişkenlere erişim veya sistem çağrıları gibi işler yapmamalı
Her Derlemede Çalışan Birim Testler
- Derleme zamanı fonksiyon değerlendirmesi olan CTFE mümkün hâle gelirse, birim test yaklaşımı da değişebilir
- C kodunda birim testlerin seyrek olmasının nedeni, ayrı build target ve çalıştırılabilir dosya oluşturup çalıştırma zahmetidir
- Örnek kod
_Static_assert(sum(3, 4) == 7, "test #1");gcc ile derlendiğinde, statik assertion içindeki ifadenin sabit olmadığına dair hata oluşur - ImportC bu kodu derleyebilir
- Bu yöntem, derleme zamanında çalıştırılabilen fonksiyonların birim testlerini basitleştirir
- Ayrı build gerekmez
- Ek çalışma gerekmez
- Kod her derlendiğinde test çalışır
- ImportC test suite’inde de bu yöntem yaygın biçimde kullanılır
İleriye Başvurunun Yarattığı Bildirim Tekrarı
- Standart C’de derleyici yalnızca sözcüksel olarak daha önce gelen bildirimleri bildiği için ileriye başvuruya izin verilmez
- Örnek kodda
floo()daha sonra tanımlanandex()fonksiyonunu önce çağırırsa, gccdexiçin örtük bildirim ile gerçek tanımın tiplerinin çakıştığını belirten bir hata verir floovedexsırası değiştirilirse aynı kod sorunsuz derlenir- ImportC, global bildirimler hangi sırayla gelirse gelsin kabul edebilir
- İleriye başvuru olmadığında, her ileri tanım için ayrı bir bildirim eklemek gerekir
- Önce
char dex(char *s, int i);gibi bir bildirim yazılır - Ardından fonksiyon tanımı tekrar yazılmalıdır
- Önce
- Bu yöntem gereksiz tekrar işidir ve programcıyı leaf fonksiyonları üste, global arayüz fonksiyonlarını alta koyan ters sıra yerleşimi yapmaya iter
- Bu yerleşimin, bir gazete haberini aşağıdan yukarı okumaya benzediği ve mantıklı olmadığı eleştirisi yapılıyor
Header Dosyası Olmadan Bildirimleri İçe Aktarma
- C’de harici bir modül kullanmak için genellikle
.hdosyasında bildirimler oluşturmak ve bunları.cdosyasında include etmek gerekir - Örnek yapı şu şekildedir
floo.c,dex.hdosyasını include eder vedex()fonksiyonunu çağırırdex.h,char dex(char *s, int i);bildirimini içerirdex.c,dex.hdosyasını include eder vedex()fonksiyonunu tanımlar
- Her harici modül için
.hdosyası oluşturmak tekrar işini artırır .hdosyası.cdosyasıyla tam olarak eşleşmezse, neyin yanlış olduğunu bulmak çok zaman alabilir- Önerilen yöntem,
dex.cdosyasını doğrudan içe aktarmaktırfloo.ciçinde__import dex;kullanılırdex.ciçinde yalnızcachar dexx(char *s, int i) { return s[i]; }gibi tanımlar bulunur
- Bu yöntemle
.hdosyası yazmaya hiç gerek kalmaz - ImportC bu yöntemi de destekler
İlgili Belgeler
- ImportC: ImportC belgeleri
- D Language: D dili belgeleri
1 yorum
Hacker News yorumları
C dışında bir dil kullanırken en çok özlediğim şeylerden biri header dosyaları
Kamusal/özel, arayüz/uygulama ayrımının çok net olması C kodunda özellikle hoşuma gidiyor. Bir kütüphanenin yalnızca
.hdosyasına göz atarak nasıl kullanılacağını anlayabilmek iyi; genelde.hiçinde kullanım dokümantasyonu bulunur ve.cile tekrar etmez. Dokümantasyonu.ciçine koymak da mümkün ama kullanıcı açısından arayüzü okumak çok daha az konforlu hale gelirÖrneğin Rust’ta bir kütüphanenin arayüzünü ve kullanımını görmek için
cargo doc --openyeterli. Tüm herkese açık API otomatik olarak aranabilir biçimde oluşturulur; header ile kaynak arasında kodu tekrarlamaya ya da elle yönetmeye gerek kalmazYalnızca teamüllere uyulduğunda düzgün çalışıyorlar; Ada gibi arayüz ve uygulama belirtimleri iyi tasarlanmış, tekrarlı ayrıştırma gerektirmeyen dillerle karşılaştırılınca sönük kalıyorlar. C kullanmayı seviyorum ama bu kısmın daha iyi tasarlanmış olması gerekirdi
C’deki header dosyaları metni olduğu gibi araya ekleme yöntemi olduğundan “kabaca çalışıyor gibi görünen” bir düzeyde. Buna karşılık Ada’da paket ve paket gövdesi kavramları vardı; paket header dosyasına, paket gövdesi de uygulamaya karşılık geliyordu. Uzun zaman önce Ada kullandığımda, paket gövdesi uygulaması hazır olmasa bile herkes paket temelinde derleme yapabiliyordu; böylece uygulamadan önce de arayüz üzerinde uyum sağlanabiliyordu. Başka bir yönden bakınca Python’daki
importun dosya sistemine doğal biçimde eşlenmesi ve C’nin include semantiğiyle uğraşmak zorunda bırakmaması nedeniyle “header dosyası” rolünde hoşuma gidiyorHeader dosyaları olduğunda arayüzü uygulamadan farklı bir şey olarak düşünmeye başlıyorsunuz. Bu yüzden bu yaklaşıma alışkın olanlar, arayüzün araç tarafından üretildiği fikrinden rahatsız oluyor. Arayüz, uygulamanın bir yan ürünü değil; ayrıca ve bilinçli olarak tasarlanan bir şeydir ve bazıları için uygulamadan daha önemlidir. Tersine, otomatik üretilen dokümantasyona alışkın olanlar, arayüzün uygulama kaynağı adlı tek doğruluk kaynağından üretilmemesinden rahatsız olur. Ayrı arayüz dosyası olan ve olmayan dilleri uzun süre kullandıkça herkes kendi kampına yerleşir ve karşıt biçimde düşünmenin hissini unutur
Modula-2, Modula-3, Ada, Standard ML, Caml Light, OCaml, F#, D gibi ayrı tutarak ya da Object Pascal, D, Haskell, Java, C#, F#, Swift, Go, Rust gibi metin veya grafik araçlarla üreterek. Hepsi daha güçlü tiplere, daha hızlı derlemeye ve düzgün ad alanlarına sahip. Rust ve Swift toolchain’lerinde hâlâ elden geçirilmesi gereken yerler var ama C araçları, Bell Labs dışında olup bitenlere kıyasla her zaman ilkel kaldı. AT&T bunu ticari olarak değerlendirebilseydi tarih farklı olurdu; sonuçta olgun portakallar yerine bedava limonlar almış olduk. Yine de C için TypeScript benzeri bir şey yaptılar ve bugün sınır denetimli koleksiyon tiplerini ve düzgün modülleri de destekliyor
Bu alanda yazarın çok daha fazla bilgi ve deneyimi olduğundan, aşağıdaki sorunları nasıl çözeceğini merak ediyorum
Sabit ifade değerlendirmesi yalnızca çeviri birimi içinde yapılırsa basitleşir ama kod tekrarı olmadan yapılabilecek şeyler ciddi biçimde sınırlanır. Derleme zamanı birim testleri, testler makro olarak ifade edilebiliyorsa bir ölçüde mümkün; önceki madde de eklenirse kolaylaşır. Bildirimlerin ileri referansı büyük tepki çekebilir; çünkü derleyici 1 geçişten 2 geçişe değişir ve bunun performans etkisi olur. Dev kod tabanlarını derlerken çeviri birimi paralelleştirmesi de yapanlar için bunu kabul etmek zor olabilir. Bildirim içe aktarma uyumluluğu bozan bir değişikliktir. C’de bir değişken tanımladıktan sonra bir
.cdosyasını import edip, değişkeni değiştirip aynı.cyi tekrar import ederek şablona benzer bir şey uyguladığım olmuştu. SQLite C Amalgamation’ın önüne çeşitli tanımlar koyup iç fonksiyonları açığa çıkarmak için fonksiyon eklediğim de olmuştu; bu kullanım örneklerinin hepsi bozulacak gibi görünüyor. Bu sorunlar için bir çözüm olup olmadığını merak ediyorumC’de bu
#includeile yapılabilir; D’de ise gerekli kodun bulunduğu modül import edilerek işlenir. Testleri makro olarak ifade etme yöntemi, bir fonksiyonu test etmek istediğiniz durumlar için uygun değildir. Örnek, anlaşılmayı kolaylaştırmak için basit tutuldu; gerçek kullanım çok daha karmaşık hale gelebilir. Performans açısından D, C derleyicisinden daha hızlı derlenir; başlıca nedeni C önişlemcisinin birden fazla geçiş gerektiren fosilleşmiş bir yapı olması ve D’nin#includeyerine import kullanarak.hdosyalarını tekrar tekrar derlememesidir. D’nin stratejisi ayrıştırma ile anlamsal analizi ayırmaktır; biraz yavaş olabilir ama yinelenen bildirimleri yeniden derleyip tekilleştirme maliyeti yoktur. Derleme zamanı fonksiyon yürütme çok kullanılırsa darboğaz olabilir; hafif kullanıldığında ise performans gayet iyidir. C’de şablonları hack’leyerek uyguluyorsanız zaten dilin ötesine geçmişsiniz demektir ve daha güçlü bir dile ihtiyacınız vardır. D, en üst düzey metaprogramlamaya sahiptir ve diğer şablon dilleri de çoğu zaman D’nin yolundan ilerlerBağımlılık grafiğinin artık topolojik sıralama düzeninde yer aldığına güvenemiyorsunuz
Modern C derleyicilerinin gerçekten hâlâ tek geçişli olup olmadığını da merak ediyorum
Yazıdaki sabit ifade değerlendirmesi örneği epey basit, ama daha karmaşık durumlarda derleyicinin hızı ve bellek kullanımı ciddi ölçüde kötüleşir; bundan yararlanmak için de bir sanal makine gerekecek gibi görünüyor.
Bu yüzden bunun standarda koymak için “fazla karmaşık” görülmüş olabileceğini anlayabiliyorum. C++20’de tanımlanan tuhaf karışım yerine C++ veya C’nin bildirimleri içe aktarma yönüne gitmesini isterdim. Örneğin
#import "string.c" as strgibi bir modülü bir sembol adıyla içe aktarıp dosyadaki tüm statik olmayan sembollerestr.trim(" Hello World ");şeklinde erişilebilmesi gibi. Ayrı olarak,__import dex;gibi dosya yolunun açıkça belirtilmediği şeyleri sevmiyorum. Bu durumdadex.dmi yoksadex.cmi içe aktarılıyor bilemezsinizÇok kullanılırsa elbette derleme süresi ve bellek harcar. Sanal makine konusuna gelince, sabit katlama zaten başlı başına bir sanal makinedir; burada yapılan, buna işlev çağrısı işlemeyi genişletmek olur. C’nin semantiği basit olduğu için o kadar kötü değil. Önerdiğiniz import yöntemi D import’un yaptığı işle neredeyse aynı: https://dlang.org/spec/module.html#import-declaration.
dex.dmidex.cmi sorunu gerçekten ortaya çıkar; bunun yanıtı import yolu ayarıdır. C derleyicilerindeki include yoluna benzerC’de zaten sabit ifadeler var. Daha büyük engel, derleyicinin işlevin kaynak koduna erişmesi gerektiği; bu yüzden muhtemelen aynı çeviri birimindeki işlevlerle sınırlı kalır. Ayrıca çeşitli derleyici temsilcilerinden oluşan bir komitenin böyle bir sabit değerlendirmenin semantiği üzerinde anlaşması gerektiği insan tarafı da çok daha büyük bir sorun olabilir
constexprözelliğiyle neredeyse bire bir aynı görünüyorNeredeyse tüm C derleyicileri zaten aynı zamanda C++ derleyicisi de olduğundan, C’de
constexprişlevleri ve değerlendirmeyi desteklemenin o kadar da kötü olup olmayacağını merak ediyorumC kodu için her zaman birim testleri yazarım
İyi bir derleme sistemi kullanır ve biraz boilerplate’e katlanırsanız zor değil.
npykütüphanesinin testleritest_load_uint8()içindenpy_load("tests/npy/uint8.npy")çağırır, boyutları, büyüklüğü ve tipiasserteder, ardındannpy_freeyapar;mainiçinde dePRINT_RUN(test_load_uint8);gibi çalıştırılır. Ön işlemciyle testlerin bir kısmı üretilebilir, ama ben basit tutmayı tercih ederimImportC derleyicisinin birim test örnekleri
_Static_assert(sizeof(struct S22079){1,2,3} == sizeof(int)*3, "ok");ve_Static_assert(sizeof(struct S22079){1,2,3}.a == sizeof(int), "ok");gibi. Semantiği derleme zamanında kontrol ettiği için bağlamaya ve çalıştırmaya gerek yok. Test sayısı arttıkça bu yöntem epey hızlanır; test paketi ne kadar hızlı dönerse üretkenlik de o kadar artarBir dönem gösterişli birim test framework’lerini derinlemesine kurcaladım, ama pek fayda sağlamadıklarını fark edip neredeyse aynı biçime yerleştim. Başkalarının hoş karşılamayabileceği nokta, statik işlevleri çağırmak için test edilen
.cdosyasını isteyerek#includeetmem. Yalnızca tek bir.cdosyası include ediyorum. Ayrıca#includeöncesinde küçük bir ön işlemci işlemi yaparak, birinin “release modu”nda derleyipassert’leri devre dışı bırakması durumunu önlemek içinNDEBUG’nin tanımlı olmamasını sağlıyorumBenzer bir şey kullanıyorum: https://github.com/ensisoft/detonator/blob/master/base/test_....
boost.test.minimal’dan çok şey ödünç aldım; aslında tek header’dı, ama yıllar içinde tek bir çeviri birimi daha eklemek gerekti. Kod tabanını testlerin her zaman geçtiği bir durumda tutarsanız, test aracının hata raporlaması ya da hata toleransı gibi karmaşıklıklara çok daha az ihtiyaç kaldığı sonucuna vardımDerleme zamanı birim testleri, “kullanılmayan import/değişken/sonuç” durumlarını uyarı değil hata yapmak kadar kötü bir fikir
Geliştiricinin elinden kontrolü alan ve sonunda işi bitirmek için bürokratik prosedürlerden geçmeye zorlayan bir “dadı özelliği”. Bu tür derleme başarısızlığı testleri “artık bitti gibi” derlemeleri için iyi olabilir, ama derlemelerin %99’unu oluşturan “hâlâ üzerinde çalışıyorum” derlemelerine uygun değil. “Matkabı kaldırmadan masa testeresini kullanamazsın” demeye benziyor
Bir düşünceyi ifade etmeye çalışırken derleme zamanı testi bunun yanlış olduğunu söylüyorsa, aslında o düşünce hâlâ eksik olabilir ya da o ifadenin tüm sonuçlarını yeterince düşünmemiş olabilirsiniz. Haskell’in tip denetimine benzer. Tip denetiminden geçmeyen bir program derlenemez ve programcıyı her zaman eksiksiz düşünceler ifade etmeye zorlar. Teorik olarak bu, daha iyi düşünülmüş programlara yol açabilir. Ancak programcıyı “geçersiz olduğunu biliyorum ama umursamıyorum” dediği köşeleri bile bulmaya zorladığı için yazmayı zorlaştırır
static_assert’e daha yakın olabilir ve kütüphane işlevlerinin uyumsuz kullanımlarını yakalamada büyük değer taşırBence oldukça iyi bir fikir
“leaf fonksiyonların önce, genel arayüz fonksiyonlarının en sonda gelmesi” bana ters geliyor
Birçok nedenle kodu topolojik sıralama düzeninde yazmayı tercih ediyorum. Bu, bir fonksiyonun içinde kod yazma biçimine benziyor; modül içinde fonksiyonların nereye konacağı netleşiyor ve en önemlisi, modülün içindeki kod parçaları arasındaki döngüsel bağımlılıklar çok belirgin hale geliyor. Döngüsel bağımlılıkları pek sevmiyorum; kod tabanını daha da birbirine doluyor ve modülleri bağımsız birimler olarak anlamayı zorlaştırıyor. Python’da, çalıştırma zamanına kadar ortaya çıkmayan sorunlar da yaratabilir[0]; döngüsel import’lar o kadar yaygın ki mevcut tip denetleyicileri bu tanılamayı varsayılan olarak kapatıyor gibi görünüyor[1]. İleri referansı desteklemeyen C, OCaml, SML gibi diller, döngüsel bağımlılıklara “en az şaşkınlık ilkesi”ni uygulamayı sağlıyor. OCaml,
let rec fn1 = .. and fn2 = ..gibi bildirilmedikçe fonksiyonlar arası özyinelemeli bağımlılığı bile yasaklıyor; yazarken biraz can sıkıcı olsa da okurken önemli bir bilgi haline geliyor[0]: https://gist.github.com/Mark24Code/2073470277437f2241033c200...
[1]: https://microsoft.github.io/pyright/#/configuration?id=type-... (
reportImportCyclesbölümüne bakın)Derleyicinin yalnızca sözcüksel olarak daha önce gelenleri bildiği için leaf fonksiyonların önce, genel arayüz fonksiyonlarının en sonda geldiği “aşağıdan yukarıya” bir sıra oluştuğuna dair bir açıklama var; ancak Python gibi ileri referansa izin veren dillerde de bu sıra yaygın[0]
Bunun ileri referansa izin vermeyen dillerden kalma bir miras mı, yoksa belirli türden kodlar için gerçekten daha mantıklı bir yöntem mi olduğunu merak ediyorum
[0] https://stackoverflow.com/a/73131538
Sadece fonksiyon gövdesindeki tanımlayıcılar, fonksiyonun kendisi çalıştırılana kadar çözümlenmez; o anda da modül kapsamındaki her şey zaten tanımlanmış olur. Fonksiyon gövdesine herhangi bir ad koyup modülü yükleyerek bunu doğrudan kontrol edebilirsiniz
Katı bir kuralım yok ama böyle okumanın daha temiz ve mantıklı olduğunu düşünüyorum. Özellikle uygulama büyükse, temelde ne yaptığını görmek için önce tamamını kaydırmak istemem. Başkaları nasıl yapıyor merak ediyorum
C’nin yapmasını istediğim “bariz şeyler” arasında işaretçi ve uzunluğu tutan slice tipi desteği, global durum kullanan API’lerin yeniden girilebilir ve mümkünse iş parçacığı güvenli sürümleri, Go veya Zig’deki
deferya da GCC’nincleanupözniteliği gibi bir şeyin standartlaştırılması ve Unicode ile UTF-8 için taşınabilir destek varC söz diziminde sabit ifadenin geçtiği her yerde, fonksiyon I/O yapmadığı, değiştirilebilir global değişkenlere erişmediği, sistem çağrısı vb. gerçekleştirmediği sürece derleyicinin fonksiyonu derleme zamanında çalıştırabilmesi gerektiği fikri kolayca bozulabilir
Çok uzun süren saf bir fonksiyon seçmek yeterli. Örneğin
int busybeaver(int n) {...}n durumlu meşgul kunduz makinesinin azami ömrünü döndüren saf bir fonksiyonsa,int x = busybeaver(99);gibi bir kod sorun olurC23’te
constexprvar ama henüz fonksiyonlara uygulanamıyorAncak bir öneri var: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2976.pdf