- แม้ใน C ก็สามารถสร้าง โครงสร้างข้อมูลเจเนอริกที่ปลอดภัยด้านชนิด ได้ด้วยการผสม macro,
void *, flexible array member และ union โดยตัวอย่างนี้แสดงการทำแบบเป็นขั้นตอนด้วย linked list
- วิธี include header ตามชนิดหลายรอบนั้นปลอดภัย แต่เพราะมีโค้ดที่สร้างจาก macro จึงทำให้ การตามหาจุดที่ประกาศนิยามและการเติมโค้ดอัตโนมัติ ยากขึ้น และอาจทำให้ขนาดไบนารีกับเวลา build เพิ่มขึ้น
- ลิสต์ที่อิง
void * มีความยืดหยุ่นสูง แต่ไม่สามารถป้องกันความผิดพลาดของชนิดข้อมูลได้ และหากจัดสรร node กับข้อมูลแยกกันก็อาจเกิด การจัดสรร 2 ครั้งต่อ node และ cache miss ได้
- หากใช้ flexible array member เพื่อเก็บข้อมูลไว้ภายใน node แล้วห่อ
List(type) ด้วย union ก็จะสามารถแนบ ข้อมูลชนิดในช่วงคอมไพล์ ได้โดยไม่มีต้นทุนตอนรันไทม์
- macro
list_prepend ใช้ตัวดำเนินการ ternary เพื่อตรวจให้ค่าที่ส่งเข้ามาตรงกับชนิดของ payload และบังคับให้เกิดข้อผิดพลาดตอนคอมไพล์ได้ ส่วนชนิดของ pointer ที่คืนค่ากลับสามารถใช้ __typeof__() ได้
จุดเริ่มต้นของการทำเจเนอริกใน C
- เป้าหมายคือสามารถประกาศลิสต์แยกตามชนิดแบบ
List(int), List(Foo) ใน C ได้ และหากใส่ข้อมูลผิดชนิดก็ต้อง คอมไพล์ไม่ผ่าน
- ในตัวอย่าง
List(Foo) สามารถใส่ค่า Foo ได้ แต่โค้ดอย่าง list_prepend(&foo_list, 7) ที่ใส่คนละชนิดจะคอมไพล์ไม่ผ่าน
- ภายใน
list_for(item, &foo_list) ตัวแปร item สามารถใช้งานเป็นชนิด Foo * ได้
ระดับ 0: วิธีใช้ generic header
- วิธีหนึ่งคือเขียนโครงสร้างข้อมูลไว้ใน header แล้วเปลี่ยน macro ชนิด
T พร้อม #include หลายรอบ
list.h จะสร้างชนิดและฟังก์ชันอย่าง FooListNode, Foo_list_prepend จาก macro ตาม T
- วิธีนี้ทั้ง เป็นเจเนอริก และ ปลอดภัยด้านชนิด แต่ประสบการณ์ใช้งานค่อนข้างกระด้าง
- ชนิดและฟังก์ชันถูกประกอบจาก macro ทำให้ตามหาตำแหน่งนิยามได้ยาก
- การเติมโค้ดอัตโนมัติอาจทำงานได้ไม่ดี
- ฟังก์ชันเดียวกันถูกสร้างซ้ำแยกตามชนิด ทำให้ขนาดไบนารีและเวลา build เพิ่มขึ้น
- ต้องเรียกฟังก์ชันที่มีคำนำหน้าตามชนิด เช่น
Foo_list_prepend(), int_list_prepend() แทนที่จะเป็น list_prepend() ตัวเดียว
- สำหรับฟังก์ชันเจเนอริกที่จำเป็นต้องสร้างโค้ดแยกตามชนิด วิธีนี้อาจเหมาะกว่า
ระดับ 1: ลิสต์ที่อิง void *
- หาก
ListNode มี void *data ก็จะเก็บข้อมูลได้หลายชนิด
list_prepend(ListNode **head, void *data) สามารถเก็บ data pointer ที่รับเข้ามาไว้ได้ตรง ๆ จึงเขียนได้ง่าย
- ปัญหาคือโครงสร้างนี้ ไม่ปลอดภัยด้านชนิด
- หากจัดสรร node กับข้อมูลแยกกัน ต้นทุนด้านหน่วยความจำและประสิทธิภาพก็จะสูงขึ้นด้วย
- ต้องจัดสรร 2 ครั้งต่อหนึ่ง node
- pointer
data เองก็ใช้หน่วยความจำเพิ่ม
- ตอนวนลูปลิสต์ อาจเกิด cache miss ทั้งตอนเข้าถึง node ถัดไปและตอนเข้าถึงข้อมูล
- โค้ดตัวอย่างใช้
malloc เพราะคุ้นเคยกันทั่วไป แต่ในทางปฏิบัติแนะนำให้ใช้ Arena และสามารถดูข้อมูลเพิ่มเติมได้จากวิดีโอและบทความ
ระดับ 2: เก็บข้อมูลไว้ภายใน node
- แทนที่จะใช้
void *data สามารถใช้ Flexible Array Member เพื่อวางข้อมูลไว้ภายใน node ได้
struct ListNode จะมี ListNode *next และ char data[] โดยตอนจัดสรรจะขอหน่วยความจำครั้งเดียวเป็น sizeof(* node) + data_size
list_prepend จะรับข้อมูลและขนาดเข้ามา แล้วคัดลอกไปยัง node->data ด้วย memcpy
- วิธีนี้ทำให้
next และข้อมูลจริงวางอยู่ใกล้กันในหน่วยความจำ จึงช่วยลดปัญหาการจัดสรรและ cache ของวิธี void *
- แต่ผู้เรียกต้องมีภาระในการส่ง
data_size เข้ามาเอง
- หากไม่อยากใช้
memcpy ก็สามารถให้ list_alloc_front คืน pointer ไปยังพื้นที่ข้อมูลของ node เพื่อให้ผู้เรียกกำหนดค่าในหน่วยความจำนั้นได้โดยตรง
- ประเด็นเรื่อง alignment, padding และการคำนวณขนาดของสมาชิก
data เป็นอีกหัวข้อหนึ่ง จึงไม่ได้ลงรายละเอียดในตัวอย่างนี้
ระดับ 3: แนบข้อมูลชนิดด้วย union
- เทคนิคหลักคือกำหนด
List(type) เป็น union แล้วเก็บทั้งหัวลิสต์จริงและ pointer สำหรับข้อมูลชนิดไว้ด้วยกัน
#define List(type) union { \
ListNode *head; \
type *payload; \
}
payload ไม่ได้ใช้ตอนรันไทม์ แต่มีไว้เพื่อให้ ข้อมูลชนิดในช่วงคอมไพล์
- เนื่องจากใช้
union จึงไม่ทำให้ payload ใช้หน่วยความจำเพิ่ม
- จึงสามารถสร้างลิสต์ตามชนิดอย่าง
List(Foo) foo_list, List(int) int_list ได้
ตรวจชนิดด้วยตัวดำเนินการ ternary
- macro
list_prepend จะเรียกฟังก์ชันภายใน _list_prepend พร้อมใช้ตัวดำเนินการ ternary เพื่อบังคับให้ชนิดของ item ตรงกับ (list)->payload
#define list_prepend(list, item) \
_list_prepend(&((list)->head), \
(1 ? (item) : (list)->payload), \
sizeof(*(list)->payload))
- หากชนิดของ candidate ทั้งสองฝั่งในตัวดำเนินการ ternary ไม่ตรงกัน คอมไพเลอร์จะรายงานข้อผิดพลาดเรื่องชนิดไม่ตรงกัน
- ตัวอย่างเช่น หากส่ง
Bar * เข้าไปใน List(Foo) ทาง Clang จะรายงานว่า pointer ชนิด Foo * กับ Bar * ไม่เข้ากัน
- macro เดียวกันยังส่งขนาดของชนิดที่จะเก็บให้อัตโนมัติผ่าน
sizeof(*(list)->payload)
- งานจริงจะถูกจัดการโดย ฟังก์ชันภายในแบบเจเนอริก อย่าง
_list_prepend(ListNode **head, void *data, size_t data_size)
ใช้ __typeof__() กับชนิดคืนค่า
- เมื่อฟังก์ชันเจเนอริกต้องคืน internal data pointer ก็สามารถใช้
__typeof__() เพื่อแปลงค่าที่คืนจาก void * ให้เป็นชนิดของ payload ได้
#define list_alloc_front(list) \
(__typeof__((list)->payload))_list_alloc_front(&(list)->head, sizeof(*(list)->payload))
__typeof__() รองรับใน Clang, GCC และ MSVC 19.39 ขึ้นไป
- ก่อนจะถูกรวมเป็นมาตรฐานใน C23 นั้น
__typeof__() เป็นส่วนขยายแบบเลือกใช้
- ในคอมไพเลอร์ที่ไม่มี
__typeof__() เช่น MSVC ก่อน 19.39 ก็ยังใช้การตรวจชนิดด้วยตัวดำเนินการ ternary ได้
- การคืนค่าที่ปลอดภัยด้านชนิดก็ทำได้ด้วยวิธีจัดสรรผ่าน
payload เช่นกัน แต่ไม่ได้ลงรายละเอียด implementation ไว้
วิธีเก่าและข้อควรระวังเชิงนิยาม
- วิธีแบบก่อนหน้านี้เรียก
_list_prepend ผ่านการ cast ไปเป็นชนิดฟังก์ชันพอยน์เตอร์ที่มี __typeof__((list)->payload) รวมอยู่ด้วย
- การเรียกผ่านฟังก์ชันพอยน์เตอร์ที่ถูก cast แบบนี้ถือเป็นพฤติกรรมที่ในทางเทคนิคแล้วไม่ถูกนิยามไว้ แต่โดยทั่วไปมองว่าไม่ได้มีปัญหาในคอมไพเลอร์และแพลตฟอร์มสมัยใหม่
- วิธีปัจจุบันจึงเปลี่ยนจากการ cast ฟังก์ชันพอยน์เตอร์มาเป็นการบังคับข้อผิดพลาดผ่าน การทำให้ชนิดในตัวดำเนินการ ternary ตรงกัน
ปัญหาเมื่อต้องส่ง List(Foo) เป็นอาร์กิวเมนต์
- คอมไพเลอร์ C อาจไม่มอง
List(Foo) สองตัวที่มีโครงสร้างเหมือนกันว่าเป็นชนิดเดียวกัน
List(Foo) a;
List(Foo) b = a; // error
- แม้จะประกาศฟังก์ชันเป็น
void my_function(List(Foo) list) แล้วเรียก my_function(a) ก็อาจเกิดข้อผิดพลาดว่าชนิดไม่เข้ากันได้
- วิธีแก้คือใช้
typedef เพื่อตั้งชื่อชนิด
typedef List(Foo) ListFoo;
ListFoo a;
ListFoo b = a; // ok
void my_function(ListFoo list);
my_function(a); // ok
- สำหรับตัวแปรภายในฟังก์ชันยังคงใช้รูปแบบ
List(Foo) local_foo_list ต่อไปได้
- ใน GCC 15 และ Clang ช่วงปลายปี 2025 จะมีการเปลี่ยนกฎที่ทำให้ชนิดที่มีโครงสร้างเหมือนกันและใช้ชื่อ tag เดียวกันถูกมองเป็นชนิดเดียวกัน
ประยุกต์ใช้กับโครงสร้างข้อมูลอื่นนอกเหนือจากลิสต์
- เทคนิคเดียวกันนี้นำไปใช้ได้ไม่ใช่แค่กับลิสต์ แต่รวมถึง map, array, binary tree และโครงสร้างข้อมูลอื่น ๆ อีกหลายแบบ
- ยังสามารถขยายไปใช้กับโครงสร้างข้อมูลที่ต้องมีหลายชนิดที่เกี่ยวข้องกันได้ด้วย
- ตัวอย่างเช่น hash map สามารถเก็บทั้งโครงสร้างภายใน ชนิดของคีย์ และชนิดของค่าไว้ใน
union เดียวกันได้
#define Map(key_type, value_type) union { \
MapInternal map; \
key_type *key; \
value_type *value; \
}
- stb_ds.h ก็เป็นอีกตัวอย่างของโครงสร้างข้อมูลเจเนอริกที่ปลอดภัยด้านชนิด แต่เนื่องจาก array และ map ใช้ C array จึงทำให้ข้อผิดพลาดด้านชนิดบางส่วนถูกตรวจพบตอนกำหนดค่าให้ array แทนที่จะเป็นตอนส่งค่าเข้าไป
2 ความคิดเห็น
หรือพูดง่าย ๆ ว่า แค่ใช้ Zig ก็พอแล้วไม่ใช่เหรอ? ก็มีความสงสัยแบบนั้นขึ้นมาเหมือนกัน
ความคิดเห็นบน Hacker News
โค้ดระดับ 2 ที่เป็น
uint64_t data[];นั้นผิดสำหรับชนิดที่มี ข้อกำหนดการจัดแนว มากกว่าuint64_tและสิ้นเปลืองสำหรับชนิดที่เล็กกว่า ตัวอย่างเช่น ilp32 ABI บนสถาปัตยกรรม 64 บิตก็เป็นกรณีแบบนั้นโค้ดระดับ 3 ควรเป็น
int main() { List(Foo) foo_list = {NULL};ถ้าไม่มี
typeofแล้วใช้วิธีอ้อม ก็จะคืนค่าอะไรไม่ได้เลย และเพราะ==เป็นแบบสมมาตร วิธีอ้อมนี้จึงยอมให้ข้อผิดพลาดเกี่ยวกับconstผ่านไปได้ด้วยpayloadก็ละทิ้งอย่างปลอดภัยไม่ได้เช่นกัน เพราะจำเป็นต่อการรู้ขนาดที่ถูกต้อง กรณีที่อยากเพิ่มint32_tเข้าไปในList(int64_t)ควรทำได้ แต่จะไม่รู้sizeofของint32_tนั้น โค้ดนี้ยังขาดอีกหลายส่วนกว่าจะทำงานได้ถูกต้องเจเนริกใน C ปัจจุบันมีข้อจำกัดใหญ่สองอย่าง อย่างแรก วิธีมอบหมายไปยัง vtable มีข้อจำกัด เพราะ struct ใส่แมโครไม่ได้ ใส่ได้แค่ฟังก์ชัน อย่างที่สอง ถ้าต้องการหลีกเลี่ยงโอเวอร์เฮด ก็ต้องมอบหมายไปยัง vtable ภายนอก และในการทำเช่นนั้นต้อง ประกาศล่วงหน้า ชนิดทั้งหมดที่จะใช้ vtable
วิธีที่ดีที่สุดที่พบมาจนถึงตอนนี้คือประกาศฟังก์ชัน static ไว้เฉย ๆ ในเฮดเดอร์ล่วงหน้าที่ประกาศ typedef แต่ไม่กำหนดนิยามไว้จริง ๆ ในทางปฏิบัติ GCC กับ Clang จะแจ้งเตือน “undefined static” ในขั้นตอนที่ต่างกัน เมื่อไม่ได้ include เฮดเดอร์ของชนิดเฉพาะใด ๆ ใน translation unit หนึ่ง
ตัวอย่างเช่น ลองนึกถึงฟังก์ชันที่รับ
struct SizedBuffer {void *p; size_t len;};หรือstruct BoundedBuffer {void *begin; void *end;};ที่มาจากเฮดเดอร์ต่างกัน รวมถึงเวอร์ชันconstของแต่ละแบบทั้งหมดเนื่องจากปัญหาที่ว่าหากจะมอบหมายไปยัง vtable ภายนอก ต้องประกาศล่วงหน้าชนิดทั้งหมดที่จะใช้ vtable ในโปรเจกต์ Apache Clownfish ที่เคยมีส่วนร่วม เราถึงขั้นสร้างคอมไพเลอร์สำหรับเรื่องนี้โดยเฉพาะ
ตอนแรกพาร์สไฟล์
.hแต่สุดท้ายเห็นว่าการสร้างภาษาเฮดเดอร์ขนาดเล็กชื่อ.cfh“Clownfish Header” น่าจะดีกว่าเพื่อเรียกเวอร์ชัน
CharBufของเมธอดCloneที่นิยามอยู่ในคลาสแม่Objจึงสร้างโค้ดแบบนี้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);}การใช้งานเป็นแบบนี้
cfish_CharBuf *charbuf = cfish_CharBuf_new();cfish_CharBuf *clone = CFISH_CharBuf_Clone(charbuf);เป้าหมายของ Clownfish คือให้ object model ที่เป็นตัวหารร่วมขั้นต่ำสำหรับ binding ของภาษาไดนามิกหลายภาษา และไฟล์
.cfhก็ถูกใช้เพื่ออนุมานชนิดสำหรับภาษาที่ทำ binding ด้วย ถึงอย่างนั้น ปริมาณ โค้ดสำเร็จรูป ที่ต้องสร้างขึ้นเพื่อหลีกเลี่ยงปัญหาที่ชี้มาก็ยังมากจนน่าขันจริง ๆดังนั้นแทบทุกคนจึงยอมละทิ้งความปลอดภัยของชนิด แล้วใช้การแคสต์
void*กับเป้าหมายที่เรียกโดยตรงhttps://github.com/apache/lucy-clownfish
ใน C นั้น
int main()ไม่ได้หมายความว่าไม่รับอาร์กิวเมนต์ แต่หมายความว่ารับ อาร์กิวเมนต์จำนวนไม่ทราบแน่ชัด ถ้าต้องการหมายความว่าไม่รับอาร์กิวเมนต์ ต้องเขียนint main(void)เรื่องนี้คนที่ใช้ C++ มักลืมกันบ่อยคงจะดีถ้า
unionสามารถ ขยายแบบรวมกันได้ คือเป็นวิธีที่ชนิดหนึ่งประกาศตัวเองได้ว่าเป็นส่วนหนึ่งของ union เดียวกับชนิดอื่น โดยไม่ต้องประกาศชนิดที่เป็นไปได้ทั้งหมดไว้ล่วงหน้าในที่เดียวmalloc(sizeof(*node) + data_size);ก็อาจมีปัญหาเพราะ padding ได้เช่นกัน ขนาดที่คำนวณได้อาจเล็กเกินไปไม่เห็นด้วย
เคยสร้างภาษาถิ่นของ C ทั้งชุดด้วย trick#0 ที่บทความพูดถึง ตัวอย่างเช่น generic binary heap อยู่ที่ https://github.com/gritzko/librdx/blob/master/abc/HEAPx.h
ไวยากรณ์ค่อนข้างหนักหน่วง แต่ข้อดีใหญ่คือสิ่งที่ได้ท้ายที่สุดเป็น struct ของ C ธรรมดา ที่เรียบง่าย คาดเดาได้ และปรับให้เหมาะสมได้ง่าย เป็นโค้ดที่คอมไพเลอร์กลืนได้สบายเหมือนโดนัท
วิธีอื่น ๆ ท้ายที่สุดก็ต้องใช้
void*และการคำนวณขนาดหน่วยความจำตอนรันไทม์ และยังไงก็ต้องนิยามแมโครอยู่ดีถ้าจะใช้ generic binary heap ก็อาจชั่งน้ำหนักตัวเลือกต่างออกไป เรื่องนี้ก็พูดถึงไว้ในเชิงอรรถแล้ว
เนื่องจากแต่ละอินสแตนซ์ถูก monomorphize คอมไพเลอร์จึงมีโอกาส optimize ได้มากกว่า และไม่ต้องจ่ายต้นทุนตอนรันไทม์จากขนาดที่แปรผัน เพราะเป็นขนาดคงที่ จึงสามารถวาง generic struct ไว้บน stack ได้ด้วย
ปัญหาที่ผู้เขียนกล่าวถึงอย่างน้อยสองอย่างสามารถเลี่ยงได้ ชื่อสามารถเปลี่ยนจาก
Bar_func(args…)เป็นfunc(Bar)(args…)ได้ด้วยแมโคร name mangling ง่าย ๆ ส่วน binary bloat อาจลดได้บางส่วนโดยใช้ weak symbol เพื่อให้ตอนลิงก์ลบฟังก์ชันที่ซ้ำกันและแชร์กันระหว่าง translation unit ได้generic container ของชนิด pointer มีปัญหาอื่นอยู่ แต่เลี่ยงได้ด้วย typedef หรือ type alias
ใน C โครงสร้างข้อมูลแบบ intrusive ยังสะดวกกว่าอยู่ แต่จัดการใน debugger แล้วทรมานมาก
การแคสต์ชนิดของฟังก์ชันตั้งสมมติฐานว่าชนิดพอยน์เตอร์ของรายการ เช่น
Foo*มี representation แบบเดียวกับvoid*แต่ C standard ไม่ได้รับประกันสิ่งนี้ ในถ้อยคำของมาตรฐาน สองชนิดนี้ไม่ได้ “compatible” กันดังนั้นการเรียกฟังก์ชันด้วยชนิดที่ถูกแปลงแล้วจึงเป็น undefined behavior แม้ representation ของพอยน์เตอร์จะบังเอิญเหมือนกัน ก็ยังส่งผลต่อ alias analysis ของคอมไพเลอร์ด้วย เรื่องที่เกี่ยวข้องกัน [0] ก็น่าอ่าน
การแคสต์ฟังก์ชันให้มีชนิดอาร์กิวเมนต์ต่างกันดูเหมือนเป็นหัวใจของ type safety ในการเรียกแบบ generic แต่ไม่แน่ใจว่านี่เป็นปัญหาที่แก้ได้หรือไม่
https://news.ycombinator.com/item?id=44421185
ถ้าต้องการ “C ที่มี generic” ทำไมไม่ใช้ C++ ไปเลย แทนที่จะอ้อมขนาดนี้
อย่างไรก็ตาม สำหรับโปรเจกต์ใหม่ เราสามารถกำหนดมาตรฐานและความคาดหวังให้ใช้ C++ ได้ และในทางปฏิบัติก็ทำแบบนั้น พร้อมกำหนดให้เล็งไปที่
stdบางเวอร์ชันเห็นท่าทีแบบนี้ใน Hacker News ค่อนข้างบ่อย ซึ่งให้ความรู้สึกคล้าย ๆ “ไปเพิ่มฝีมือซะ” ผมคิดว่าเรื่องนี้ต้องการบริบทมากกว่านั้นมาก
หลังจาก Microsoft หันมาชอบ Linux และซอฟต์แวร์เสรี/โอเพนซอร์สมากขึ้นแล้ว การถอยจากจุดยืนที่ว่า “C++ คืออนาคต” นั้นน่าผิดหวังจริง ๆ
https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...
https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-...
ทุกวันนี้ เนื่องจากกฎระเบียบของรัฐบาลและกฎหมายไซเบอร์ ทำให้ Microsoft มีนโยบายใหม่เกี่ยวกับ C และ C++ แล้ว เรื่องนี้จึงไม่ได้สำคัญมากนัก
https://azure.microsoft.com/en-us/blog/microsoft-azure-secur...
https://blogs.windows.com/windowsexperience/2024/11/19/windo...
เป็นทริกที่เยี่ยม ผมใช้ในไลบรารีทดลองของตัวเองอยู่แล้วด้วย https://github.com/uecker/noplate/blob/main/src/list.h
หมายถึงแทนที่จะใส่ข้อมูลไว้ในโหนดแบบตอนนี้ ก็ใส่ struct ของโหนดไว้ในข้อมูลแทน และเป็นผลพลอยได้ที่ทำให้ออบเจ็กต์หนึ่งตัวอยู่ในหลายคอนเทนเนอร์ได้
ตรงที่ว่า “ชนิดที่มีโครงสร้างเหมือนกันจะถือว่าเป็นชนิดเดียวกันใน GCC 15 และ Clang ช่วงครึ่งหลังของปี 2025 ด้วยการเปลี่ยนกฎ” ต้องระวัง
ภายใต้กฎใหม่ สิ่งที่จะถือว่าเป็นชนิดเดียวกันมีแค่ union ที่มี tag เท่านั้น และต้องมีโครงสร้างเหมือนกันกับ tag เหมือนกันด้วย
มาโคร
List(T)ต้องเปลี่ยนให้สร้าง tag ที่ต่างกันสำหรับTแต่ละตัว ถ้าเป็นชนิดคำเดียวธรรมดาก็ง่ายด้วย##แต่ถ้าซับซ้อนขึ้นนิดเดียว เช่น พอยน์เตอร์charหรือก็คือสตริง ก็ทำไม่ได้แล้วแน่นอนว่าสามารถบังคับให้ typedef ทุกชนิดก่อนใช้กับ
Listได้ แต่แบบนั้นความเป็น generic จะลดลงมากtypedef char *str;List(str) my_list_of_str;List(str) tokenize(str input) {...}คำทั่วไปสำหรับ “สมาชิกที่ไม่ทำอะไรและมีไว้ถือชนิดเท่านั้น” ผมว่าเรียกว่า type witness แต่เอกสารเกี่ยวกับ type witness มีน้อยกว่าที่คิดมาก
ส่วนใหญ่เห็นใน Haskell และเคยใช้ใน Scala เพื่อจำลองลำดับชั้นของชนิดที่ไม่มีอยู่จริงใน type system
ในบางแง่ ทริก union นี้ก็คล้ายกับ phantom type เพราะชนิดช่วยประกอบไม่ได้ถูกใช้งานจริงเลย
มีวิธีที่ใช้ใน Linux kernel ด้วย คือ embed
struct list_headซึ่งเป็นข้อมูลของลิสต์ไว้ใน struct ของแต่ละชนิดhttps://kernelnewbies.org/FAQ/LinkedLists
LIST_HEAD_INITกับINIT_LIST_HEADชวนสับสนถ้าต้องทำถึงขนาดนี้ ผมคงใช้ C++ template ตรง ๆ ไปเลย
ใน D ทำแบบนี้ได้
struct ListNode(T) {ListNode* next;T data;}T!int node;ทำไมต้องลำบากกับพรีโปรเซสเซอร์ของ C ด้วย? การใช้มาโครพรีโปรเซสเซอร์ก็เหมือนใช้ค้อนแทนปืนยิงตะปูในงานไม้ขั้นสุดท้าย ปืนยิงตะปูเร็วกว่า 10 เท่า ยิงตะปูได้แม่นทุกครั้ง และไม่ทิ้งรอยบุ๋มรูปพระจันทร์เสี้ยวไว้บนชิ้นงาน