- C’de de makrolar,
void *, flexible array member ve union birleştirilerek tip güvenli jenerik veri yapıları oluşturulabilir; örnek, bağlı listeyi adım adım nasıl uygulayacağını gösteriyor
- Tipe özel başlık dosyasını birden çok kez include etme yöntemi güvenli olsa da, makroyla üretilen kod nedeniyle tanım takibi ve kod tamamlama zorlaşır; ikili dosya boyutu ve derleme süresi de artabilir
void * tabanlı liste genel amaçlıdır ama tip hatalarını engelleyemez; düğüm ve veri ayrı ayrı ayrılırsa düğüm başına 2 ayırma ve cache miss oluşabilir
- Flexible array member ile veriyi düğümün içinde saklayıp
List(type)’ı union ile sararsanız, çalışma zamanı maliyeti olmadan derleme zamanı tip bilgisi ekleyebilirsiniz
list_prepend makrosu, üçlü operatörle aktarılan değer ile payload tipini eşleştirerek derleme hatası üretir; dönüş işaretçisi tipi için __typeof__() kullanılabilir
C jenerik uygulamasının başlangıç noktası
- Amaç, C’de
List(int), List(Foo) gibi tipe özel listeler tanımlamak ve yanlış tip eklendiğinde bunun derlenmemesini sağlamaktır
- Örnekte
List(Foo) içine Foo değeri konabilir, ancak list_prepend(&foo_list, 7) gibi farklı tip ekleyen kod derlenmez
list_for(item, &foo_list) içindeki item, Foo * tipi olarak ele alınabilir
Seviye 0: Jenerik başlık dosyası yöntemi
- Bir yöntem, veri yapısını bir başlık dosyasına yazıp tip makrosu
T’yi değiştirerek birden çok kez #include etmektir
list.h, T’ye dayanarak FooListNode, Foo_list_prepend gibi tipleri ve fonksiyonları makroyla üretir
- Bu yöntem jenerik ve tip güvenlidir, ancak kullanım deneyimi pürüzlü hale gelir
- Tipler ve fonksiyonlar makrolarla oluşturulduğu için tanım konumunu bulmak zorlaşır
- Kod tamamlama iyi çalışmayabilir
- Her tip için aynı fonksiyonun bir kopyası oluştuğundan ikili dosya boyutu ve derleme süresi artar
- Tek bir
list_prepend() yerine Foo_list_prepend(), int_list_prepend() gibi tip önekli fonksiyonlar kullanmak gerekir
- Tipe özel kod üretimi gerektiren jenerik fonksiyonlarda bu yöntem daha uygun olabilir
Seviye 1: void * tabanlı liste
ListNode, void *data taşıyorsa birden fazla tipte veri barındırabilir
list_prepend(ListNode **head, void *data), veri işaretçisini olduğu gibi sakladığı için uygulaması basittir
- Sorun, bu yapının tip güvenli olmamasıdır
- Düğüm ve veri ayrı ayrı ayrılırsa bellek ve performans maliyeti de artar
- Tek bir düğüm için iki ayırma gerekir
data işaretçisinin kendisi ek bellek kullanır
- Listeyi dolaşırken sonraki düğüme erişimde ve veriye erişimde ayrı ayrı cache miss yaşanabilir
- Örnek kod, aşinalık nedeniyle
malloc kullanıyor; ancak pratikte Arena kullanımı öneriliyor ve ilgili kaynak olarak video ile yazı incelenebilir
Seviye 2: Veriyi düğüm içinde saklama
void *data yerine Flexible Array Member kullanılırsa veri düğümün içine konabilir
struct ListNode, ListNode *next ve char data[] içerir; ayırma sırasında sizeof(* node) + data_size kadar alan tek seferde ayrılır
list_prepend, aktarılan veriyi ve boyutu alıp memcpy ile node->data içine kopyalar
- Bu yöntem,
next ile gerçek veriyi bellekte birbirine yakın yerleştirerek void * yöntemindeki ayırma ve cache sorunlarını azaltır
- Buna karşılık çağıranın
data_size geçirme yükü doğar
memcpy’den kaçınmak isterseniz list_alloc_front, düğümün veri alanı işaretçisini döndürebilir ve çağıran bu belleği doğrudan ilklendirebilir
data üyesinin hizalaması, padding’i ve boyut hesaplama sorunları ayrı bir konu olduğundan örnekte ayrıntılı ele alınmaz
Seviye 3: union ile tip bilgisi ekleme
- Temel teknik,
List(type)’ı union olarak tanımlayıp gerçek liste başını ve tip bilgisi için bir işaretçiyi birlikte tutmaktır
#define List(type) union { \
ListNode *head; \
type *payload; \
}
payload çalışma zamanında kullanılmaz, derleme zamanı tip bilgisi sağlar
union kullanıldığı için payload ayrı bellek tüketmez
List(Foo) foo_list, List(int) int_list gibi tipe özel listeler oluşturulabilir
Üçlü operatörle tip denetimi yapma
list_prepend makrosu, içerideki _list_prepend fonksiyonunu çağırırken üçlü operatörle item ve (list)->payload tiplerini eşleştirir
#define list_prepend(list, item) \
_list_prepend(&((list)->head), \
(1 ? (item) : (list)->payload), \
sizeof(*(list)->payload))
- Üçlü operatörün iki aday tipi uyuşmazsa derleyici tip uyumsuzluğu hatası verir
- Örneğin
List(Foo)’ya Bar * geçirilirse Clang, Foo * ile Bar * arasındaki işaretçi tipi uyumsuzluğunu hata olarak gösterir
- Aynı makro,
sizeof(*(list)->payload) ile saklama tipinin boyutunu da otomatik geçirir
- Asıl işi
_list_prepend(ListNode **head, void *data, size_t data_size) gibi bir jenerik iç fonksiyon üstlenir
Dönüş tipi için __typeof__() kullanma
- Jenerik fonksiyonun iç veri işaretçisini döndürmesi gerektiğinde,
__typeof__() ile void * dönüş değeri payload tipine cast edilebilir
#define list_alloc_front(list) \
(__typeof__((list)->payload))_list_alloc_front(&(list)->head, sizeof(*(list)->payload))
__typeof__() Clang, GCC ve MSVC 19.39 ve sonrasında desteklenir
__typeof__(), C23’te standarda dahil edilene kadar isteğe bağlı bir uzantıydı
- MSVC 19.39 öncesi gibi
__typeof__() bulunmayan derleyicilerde üçlü operatör tabanlı tip denetimi kullanılabilir
- Tip güvenli dönüş,
payload üzerinden ayırma yöntemiyle de mümkündür; ancak ayrıntılı uygulama atlanmıştır
Eski yöntem ve tanım gereği dikkat edilmesi gerekenler
- Önceki yöntem,
_list_prepend’i __typeof__((list)->payload) içeren bir fonksiyon işaretçisi tipine cast ederek çağıran bir yapıdaydı
- Tipi cast edilmiş fonksiyon işaretçisi çağrısı teknik olarak tanımsız davranıştır; ancak modern derleyicilerde ve modern platformlarda pratikte sorun çıkarmadığı varsayılır
- Mevcut yöntem, fonksiyon işaretçisi cast’i yerine üçlü operatör tip eşleşmesi ile hatayı tetikler
List(Foo)’yu argüman olarak geçirirken yaşanan sorun
- C derleyicisi, aynı yapıya sahip iki
List(Foo) tanımını aynı tip olarak görmeyebilir
List(Foo) a;
List(Foo) b = a; // error
- Fonksiyon argümanı olarak
void my_function(List(Foo) list) tanımlayıp my_function(a) çağırıldığında da uyumsuz tip hatası çıkabilir
- Çözüm,
typedef ile tipe ad vermektir
typedef List(Foo) ListFoo;
ListFoo a;
ListFoo b = a; // ok
void my_function(ListFoo list);
my_function(a); // ok
- Yerel değişkenlerde
List(Foo) local_foo_list biçimi kullanılmaya devam edilebilir
- GCC 15 ve 2025 sonundaki Clang’da kural değişikliği ile aynı etiket adına sahip yapısal olarak aynı tiplerin aynı tip olarak kabul edilmesi planlanıyor
Liste dışındaki veri yapılarına da uygulanabilir
- Aynı teknik yalnızca listelere değil map, dizi ve ikili ağaç gibi pek çok veri yapısına da uygulanabilir
- Birden fazla ilişkili tip gerektiren veri yapılarına da genişletilebilir
- Örneğin hash map, iç yapıyı ve anahtar tipi ile değer tipini
union içinde birlikte tutabilir
#define Map(key_type, value_type) union { \
MapInternal map; \
key_type *key; \
value_type *value; \
}
- stb_ds.h da tip güvenli jenerik veri yapılarına bir örnektir; ancak diziler ve map’ler C dizilerini kullandığı için bazı tip hataları değer geçirme anında değil, dizi ataması anında yakalanır
2 yorum
Basitçe Zig kullanmak yeterli olmaz mı? diye bir soru akla geliyor.
Hacker News yorumları
Seviye 2 kodundaki
uint64_t data[];, hizalama gereksinimiuint64_t'den büyük olan türler için yanlış, daha küçük olan türler içinse israf olur. Örneğin 64 bit mimarideki ilp32 ABI böyle bir durumdur.Seviye 3 kodu
int main() { List(Foo) foo_list = {NULL};olmalı.typeofolmadığı için etrafından dolaşınca hiçbir şey döndüremezsiniz; ayrıca==simetrik olduğu için bu dolambaçlı yöntemconstile ilgili hatalara da izin verir.payloadda güvenle atlanamaz. Çünkü doğru boyutu bilmek için gereklidir.List(int64_t)'yeint32_teklemek mümkün olmalıdır, ama oint32_t'ninsizeofdeğerini bilemezsiniz. Bu kodun düzgün çalışması için hâlâ epey eksik parça var.Mevcut C jeneriklerinde iki büyük sınırlama var. Birincisi, vtable'a devreden yaklaşımda struct makro içeremez, yalnızca fonksiyon içerebilir; bu da işlevselliği sınırlar. İkincisi, ek yükten kaçınmak için harici bir vtable'a devretmek gerekir; bunun için de vtable'ı kullanacak tüm türleri önceden bildirmek gerekir.
Şimdiye kadar bulduğum en iyi yöntem, typedef'i bildiren ileri bildirim header'ında static fonksiyonları yalnızca bildirmek, tanımlamamaktı. Belirli bir türün header'ını bir çeviri birimine dahil etmediğinizde “undefined static” uyarısının hangi aşamada çıktığı GCC ile Clang'de farklı.
Örneğin farklı header'lardan gelen
struct SizedBuffer {void *p; size_t len;};veyastruct BoundedBuffer {void *begin; void *end;};ile bunların her birininconstsürümlerini alan bir fonksiyon düşünün.Harici vtable'a devretmek için vtable'ı kullanacak tüm türleri önceden bildirme zorunluluğu yüzünden, daha önce katkı verdiğim Apache Clownfish projesinde bunun için başlı başına bir derleyici bile yapmıştık.
Başta
.hdosyalarını parse ediyorduk, ama sonunda.cfh“Clownfish Header” denen küçük bir header dili yapmanın daha iyi olduğuna karar verdik.Üst sınıf
Objiçinde tanımlıClonemetodununCharBufsürümünü çağırmak için şöyle kod üretiyorduk:typedef cfish_CharBuf*(*CFISH_CharBuf_Clone_t)(cfish_CharBuf* self);extern uint32_t CFISH_CharBuf_Clone_OFFSET;static inline cfish_CharBuf*CFISH_CharBuf_Clone(cfish_CharBuf* self) {const CFISH_CharBuf_Clone_t method= (CFISH_CharBuf_Clone_t)cfish_obj_method(self,CFISH_CharBuf_Clone_OFFSET);return method(self);}Kullanımı şöyleydi:
cfish_CharBuf *charbuf = cfish_CharBuf_new();cfish_CharBuf *clone = CFISH_CharBuf_Clone(charbuf);Clownfish'in amacı, birden fazla dinamik dil bağlaması için en küçük ortak payda niteliğinde bir nesne modeli sağlamaktı;
.cfhdosyaları bağlama dilleri için türleri türetmekte de kullanılıyordu. Yine de işaret edilen sorundan kaçınmak için üretilen boilerplate kod miktarı gerçekten akıl almazdı.Bu yüzden neredeyse herkes tür güvenliğinden vazgeçip çağrı hedefinde doğrudan
void*cast'i kullanmayı tercih ediyor.https://github.com/apache/lucy-clownfish
C'de
int main()argüman almadığı anlamına gelmez; bilinmeyen sayıda argüman aldığı anlamına gelir. Argüman almadığını belirtmek içinint main(void)yazmak gerekir. C++ kullananların sıkça unuttuğu bir gerçek.union'ların birleşimsel olarak genişletilebilmesi iyi olurdu. Yani bir tür, mümkün olan tüm türleri tek bir yerde önceden bildirmeden, başka türlerle aynı union'ın parçasıymış gibi kendini bildirebilse.malloc(sizeof(*node) + data_size);da padding yüzünden sorun çıkarabilir. Hesaplanan boyut fazla küçük kalabilir.Katılmıyorum.
Yazıda bahsedilen trick#0 ile komple bir C lehçesi oluşturmuştum. Örneğin jenerik ikili heap https://github.com/gritzko/librdx/blob/master/abc/HEAPx.h adresinde.
Sözdizimi biraz ağır, ama sonunda elde ettiğiniz şeyin sıradan, öngörülebilir ve optimize etmesi kolay normal C struct'ları olması büyük avantaj. Derleyicinin çörek gibi yiyip bitireceği kod.
Diğer yaklaşımlar sonunda
void*ve çalışma zamanında bellek boyutu hesaplaması gerektiriyor; ayrıca zaten makro da tanımlamak zorundasınız.Jenerik ikili heap kullansaydım seçenekleri farklı tartmış olabilirdim. Dipnotta da bu noktaya değindim.
Her örnek monomorfize edildiği için derleyici optimizasyonuna da daha çok alan kalır ve değişken boyut nedeniyle çalışma zamanı maliyeti ödemezsiniz. Sabit boyutlu olduğundan jenerik struct'ı stack'e de koyabilirsiniz.
Yazarın bahsettiği sorunlardan en az ikisi aşılabilir. İsimler, basit bir name mangling makrosuyla
Bar_func(args…)biçimindenfunc(Bar)(args…)biçimine çevrilebilir. Binary şişmesi ise weak symbol kullanılarak, link sırasında çeviri birimleri arasında paylaşılan fonksiyonların tekilleştirilmesiyle kısmen azaltılabilir.Pointer türlerinin jenerik container'larında başka bir sorun var, ama typedef veya tür alias'larıyla bunun etrafından dolaşılabilir.
C'de intrusive veri yapıları hâlâ daha rahat, ama debugger'da uğraşması acı verici.
Fonksiyon tipi cast’i, öğe işaretçisi türünün, örneğin
Foo*’ınvoid*ile aynı temsile sahip olduğunu varsayar; ancak C standardı bunu garanti etmez. Standart terminolojiyle bu iki tür “uyumlu” değildir.Bu nedenle fonksiyonu dönüştürülmüş türle çağırmak tanımsız davranıştır. İşaretçi temsilleri tesadüfen aynı olsa bile bu, derleyicinin alias analizini de etkiler. Bununla ilgili [0] da bakmaya değer.
Fonksiyonları farklı argüman türleriyle cast etmek, jenerik çağrının tip güvenliğinin özü gibi görünüyor; bunun düzeltilebilir bir sorun olup olmadığını bilmiyorum.
https://news.ycombinator.com/item?id=44421185
“Jenerikleri olan C” istiyorsanız bu kadar dolambaçlı yollara girmeyip doğrudan C++ kullanmak gerekmez mi?
Öte yandan yeni projeler için C++ kullanılacak şekilde standartları ve beklentileri belirleyebiliriz; gerçekten de bunu yapıyor ve belirli bir
stdhedeflenecek şekilde karar veriyoruz.Hacker News’te bu tavrı oldukça sık görüyorum; biraz “becerini artır” demeye benziyor. Bence burada çok daha fazla bağlama ihtiyaç var.
Microsoft’un Linux’a ve özgür/açık kaynak yazılıma yeni bir sempati göstermesinden sonra bile “C++ gelecek” tutumundan geri adım atması gerçekten hayal kırıklığıydı.
https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...
https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-...
Bugünlerde hükümet ve siber mevzuat nedeniyle Microsoft’ta C ve C++ için yeni bir politika var, dolayısıyla pek önemli değil.
https://azure.microsoft.com/en-us/blog/microsoft-azure-secur...
https://blogs.windows.com/windowsexperience/2024/11/19/windo...
Güzel bir numara. Ben de deneysel kütüphanemde zaten kullanıyorum: https://github.com/uecker/noplate/blob/main/src/list.h
Mevcut durumda olduğu gibi veriyi düğümün içine koymak yerine, düğüm yapısını verinin içine koyan ve yan etki olarak bir nesnenin birden fazla container’a girebilmesini sağlayan yaklaşım.
“Yapısal olarak aynı türler, GCC 15 ve 2025’in ikinci yarısındaki Clang’da kural değişikliği sayesinde aynı tür olarak kabul edilir” kısmında dikkatli olmak gerekiyor.
Yeni kurallarda aynı tür olarak kabul edilenler yalnızca etiketli union’lardır; yapıları aynı olmalı ve etiketleri de aynı olmalıdır.
List(T)makrosunun, farklı herTiçin farklı bir etiket üretecek şekilde değiştirilmesi gerekir. Basit tek kelimelik türler için bu##ile kolaydır; ancakcharişaretçisi, yani string gibi biraz karmaşıklaştığında imkânsız hale gelir.Elbette
Listiçinde kullanmadan önce tüm türlerin typedef edilmesini zorunlu kılabilirsiniz, ama bu da genelliği ciddi biçimde azaltır.typedef char *str;List(str) my_list_of_str;List(str) tokenize(str input) {...}“Hiçbir şey yapmayıp yalnızca türü taşıyan üye” için genel terimin type witness olduğunu düşünüyorum. Ama type witness ile ilgili literatür beklediğimden çok daha az.
Bunu çoğunlukla Haskell’de gördüm; Scala’da da gerçek tip sisteminde olmayan bir tip hiyerarşisini taklit etmek için kullanmıştım.
Bir bakıma bu union numarası da yardımcı tür gerçekte hiç kullanılmadığı için phantom type’a benziyor.
Linux çekirdeğinde kullanılan bir yöntem de var. Liste bilgisi olan
struct list_head’i türe özgü struct’ın içine embed etme yöntemi.https://kernelnewbies.org/FAQ/LinkedLists
LIST_HEAD_INITveINIT_LIST_HEADadları kafa karıştırıcı.Böyle yapmak gerekiyorsa doğrudan C++ template kullanmayı tercih ederim.
D’de bunu şöyle yaparsınız:
struct ListNode(T) {ListNode* next;T data;}T!int node;C önişlemcisiyle neden uğraşalım ki? Önişlemci makroları kullanmak, ince marangozlukta çivi tabancası yerine çekiç kullanmaya benzer. Çivi tabancası 10 kat daha hızlıdır, çiviyi her seferinde tam yerine çakar ve iş parçasında yarım ay biçimli izler bırakmaz.