3 คะแนน โดย GN⁺ 2025-07-01 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้ใน 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 ความคิดเห็น

 
click 2025-07-01

หรือพูดง่าย ๆ ว่า แค่ใช้ Zig ก็พอแล้วไม่ใช่เหรอ? ก็มีความสงสัยแบบนั้นขึ้นมาเหมือนกัน

 
GN⁺ 2025-07-01
ความคิดเห็นบน 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* และการคำนวณขนาดหน่วยความจำตอนรันไทม์ และยังไงก็ต้องนิยามแมโครอยู่ดี

    • ผู้เขียนเอง binary heap กับ linked list มี use case ต่างกัน binary heap ต้องอ่านข้อมูลที่ใส่เข้าไปเพื่อเก็บให้ถูกต้อง แต่ linked list ไม่จำเป็นต้องทำแบบนั้น
      ถ้าจะใช้ generic binary heap ก็อาจชั่งน้ำหนักตัวเลือกต่างออกไป เรื่องนี้ก็พูดถึงไว้ในเชิงอรรถแล้ว
    • จริง ๆ มีเหตุผลหลายอย่างที่ควรชอบการทำ implementation ในเฮดเดอร์มากกว่า ต่างจาก macro function โค้ดในเฮดเดอร์สามารถ step into ใน debugger ได้ และ type information ที่ debugger เห็นก็ดีกว่า จึง ดีบัก ได้ดีกว่า
      เนื่องจากแต่ละอินสแตนซ์ถูก 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

    • เรื่องนี้กล่าวไว้ในเชิงอรรถแล้ว การแคสต์ไม่ใช่หัวใจของ type safety ลองอ่านบทความทั้งหมดดูก็พอ
  • ถ้าต้องการ “C ที่มี generic” ทำไมไม่ใช้ C++ ไปเลย แทนที่จะอ้อมขนาดนี้

    • เพราะทำงานกับ legacy project ที่ติดข้อกำกับด้านความปลอดภัยและ quality assurance อื่น ๆ อยู่ เราไม่สามารถแค่ปล่อยโซลูชันที่พอร์ตไป C++ ออกไปได้ ไม่ใช่แค่รีลีสหน้า แต่แม้กระทั่งรีลีสที่สิบก็ไม่ได้ ดังนั้นอาจต้องทำให้มันเดินต่อไปให้ได้จนกว่าจะทำได้จริง
      อย่างไรก็ตาม สำหรับโปรเจกต์ใหม่ เราสามารถกำหนดมาตรฐานและความคาดหวังให้ใช้ C++ ได้ และในทางปฏิบัติก็ทำแบบนั้น พร้อมกำหนดให้เล็งไปที่ std บางเวอร์ชัน
      เห็นท่าทีแบบนี้ใน Hacker News ค่อนข้างบ่อย ซึ่งให้ความรู้สึกคล้าย ๆ “ไปเพิ่มฝีมือซะ” ผมคิดว่าเรื่องนี้ต้องการบริบทมากกว่านั้นมาก
    • เพราะในหลาย use case ที่ใช้ C การย้ายไป C++ กลับต้องการ การอ้อมมากกว่า เสียอีก
    • บางคนเกลียด C++ เข้าไส้ งานประเภทนี้เลยยังออกมาเรื่อย ๆ
      หลังจาก 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...
    • คำตอบที่แท้จริงคือ เพราะทางนี้สนุกกว่า
    • ถ้าใน C ใช้ workaround ไม่กี่อย่างแล้วได้ผลลัพธ์เดียวกัน ทำไมต้องใช้ C++
  • เป็นทริกที่เยี่ยม ผมใช้ในไลบรารีทดลองของตัวเองอยู่แล้วด้วย https://github.com/uecker/noplate/blob/main/src/list.h

    • ถ้าจะมีใครรู้เรื่องนี้ ก็น่าจะเป็นคุณนี่แหละ คุณเห็นวิธีนำแนวทางนี้ไปใช้กับ intrusive data structure ไหม?
      หมายถึงแทนที่จะใส่ข้อมูลไว้ในโหนดแบบตอนนี้ ก็ใส่ 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) {...}

    • ไม่เข้าใจคำว่า “มีแค่ union ที่มี tag เท่านั้นที่จะถือว่าเป็นชนิดเดียวกัน” tagged union มันก็แค่ design pattern ไม่ใช่เหรอ
  • คำทั่วไปสำหรับ “สมาชิกที่ไม่ทำอะไรและมีไว้ถือชนิดเท่านั้น” ผมว่าเรียกว่า type witness แต่เอกสารเกี่ยวกับ type witness มีน้อยกว่าที่คิดมาก

    • มีคำที่คล้ายกันคือ phantom type ในกรณีที่มี type variable ซึ่งไม่ได้ถูกใช้เป็นชนิดของตัวแปรจริงเลย
      ส่วนใหญ่เห็นใน 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 เท่า ยิงตะปูได้แม่นทุกครั้ง และไม่ทิ้งรอยบุ๋มรูปพระจันทร์เสี้ยวไว้บนชิ้นงาน

  • บทความนี้เป็นเรื่องเกี่ยวกับ C ในบางโปรเจกต์จำเป็นต้องใช้ C
  • ไม่ใช่ว่าต้องใช้แค่ค้อนอย่างเดียว แต่ใช้ punch ร่วมด้วยก็ได้ ตอกตะปูตกแต่งด้วยค้อนให้เหลือประมาณ 1/8 นิ้ว จากนั้นใช้ punch ตอกให้จมลงไปจนสุด