4 điểm bởi GN⁺ 2023-10-10 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong năm 2023, cách viết mã C đã thay đổi đáng kể, và phong cách được tổ chức lại xoay quanh tên kiểu ngắn, loại bỏ chuỗi kết thúc bằng null, trả về struct, và biên dịch theo một đơn vị dịch duy nhất
  • Các bí danh ngắn như u8, i32, size, s8, cùng việc bỏ conststruct, là lựa chọn nhằm giảm nhiễu thị giác và gánh nặng nhận thức trong những khai báo lặp đi lặp lại
  • Thay vì chuỗi kết thúc bằng null, chuỗi được xử lý bằng fat pointer s8datalen; trong môi trường Win32·UTF-16 thì dùng thêm c16s16
  • Trong thiết kế hàm, ưu tiên trả về struct hơn tham số out, và dùng mẫu tạo giá trị trả về được khởi tạo 0 rồi chỉ đặt ok tại thời điểm thành công
  • Từ macro, assert, khai báo Win32 đến inline assembly, ưu tiên các quy tắc cục bộ dễ đọc; tuy nhiên khi đóng góp cho dự án khác thì vẫn theo phong cách của dự án đó

Thể hiện ý đồ bằng tên kiểu ngắn

  • Dùng các bí danh ngắn cho kiểu số nguyên·ký tự·kích thước con trỏ cơ bản
    • Ví dụ: u8, c16, b32, i32, u32, u64, f32, f64, uptr, byte, size, usize
  • Vì những tên này xuất hiện rất thường xuyên trong toàn bộ chương trình, tính ngắn gọn mang lại lợi ích trực tiếp cho việc đọc và rà soát
  • Không còn dùng hậu tố _t, vì giờ nó tạo cảm giác như một yếu tố gây rối mắt
  • Với tiền tố cho kiểu signed, ưu tiên i hơn s
    • s được dành lại để dùng cho tên kiểu chuỗi
  • Với kiểu kích thước, dùng size thay vì isize
    • Vì xem signed size là mặc định quan trọng hơn
    • usize chủ yếu chỉ có phạm vi hẹp khi tương tác với giao diện bên ngoài
  • b32 thể hiện rõ ý đồ là “boolean 32-bit”
    • Dùng kích thước từ tự nhiên thay cho _Bool
    • Trong thực tế, nó thường nằm trong thanh ghi hoặc vùng đệm căn chỉnh của struct
    • Khi bộ nhớ thực sự quan trọng, boolean sẽ được nén vào biến flags
  • c16 là kiểu cho ký tự UTF-16 cần thiết trên Win32
    • Nếu dựa trên char16_t, các debugger như GDB sẽ dễ hiển thị như dữ liệu ký tự hơn
    • Tên kiểu chính thức của Win32 là wchar_t, nhưng ưu tiên cách gọi nêu rõ UTF-16
  • u8 được dùng cho octet và chủ yếu cho dữ liệu UTF-8, còn byte được tách riêng cho bộ nhớ thô và kiểu aliasing đặc biệt
  • Lo ngại về các hệ thống không hỗ trợ kiểu độ rộng cố định được xem là ít tính thực tiễn
    • Những tên kiểu dài như int_fast32_t cũng bị xem là lãng phí không cần thiết
  • Khi chỉ đưa ra các đoạn mã rời rạc, không dùng riêng các bí danh này
    • Vì người đọc cần cả typedef đi kèm mới hiểu được ngữ cảnh

Quy tắc cho macro và assert

  • Macro dạng hàm dùng chữ thường
    • Ví dụ: countof(a), lengthof(s), new(a, t, n)
  • Với hằng số vẫn thích ALL_CAPS, nhưng macro dạng hàm thì chữ thường dễ đọc hơn
  • Macro dạng hàm ít gặp vấn đề namespace hơn macro thông thường
    • Có thể đồng thời có macro new() và biến·field new
    • Vì nếu không ở dạng gọi hàm thì nó sẽ không được mở rộng thành macro
  • Macro assert cho GCC và Clang dùng dạng while (!(c)) __builtin_unreachable()
  • Cách assert này không cần tách riêng cấu hình build
    • Không cần định nghĩa riêng cho debug và release build
    • Việc có hoạt động hay không được điều khiển bởi Undefined Behavior Sanitizer, tức UBSan
    • libubsan cung cấp chẩn đoán có kèm tên tệp và số dòng
    • Trong release build, nó trở thành gợi ý tối ưu hóa thực dụng
  • Nếu muốn bật assertion trong release build, đặt UBSan vào trap mode bằng -fsanitize-trap và bật ít nhất -fsanitize=unreachable
  • Về lý thuyết cũng có thể làm bằng -funreachable-traps, nhưng tại thời điểm bài viết, vài bản phát hành GCC gần đây đang bị lỗi

Những gì được lược bỏ trong khai báo

  • Không dùng const cho tham số
    • Tác giả cho rằng nó không có vai trò thực tiễn trong tối ưu hóa
    • Cũng không nhớ được trường hợp nào nó từng bắt lỗi hoặc lẽ ra đã bắt được lỗi
    • Tên tham số tốt là đủ để đóng vai trò tài liệu hóa trong prototype
  • Việc bỏ const là một thay đổi giúp tăng năng suất nhờ giảm gánh nặng nhận thức và nhiễu thị giác
  • Một ngoại lệ nhỏ là vẫn thích const như gợi ý để đặt bảng tĩnh vào vùng nhớ chỉ đọc gần mã
    • Khi cần thì sẽ bỏ const bằng cast
  • Với null pointer, dùng literal 0
    • Đây là phong cách đã dùng khoảng 7 năm
    • Dù về lý thuyết có thể có khuyết điểm, nhưng trong hàng trăm nghìn dòng mã chưa thấy trường hợp thực tế nào
  • Chỉ dùng restrict khi cần
    • Thường tổ chức mã sao cho không dùng tham số out trong vòng lặp, hoặc tránh luôn tham số out
  • Không dùng inline
    • Vì mọi thứ được biên dịch thành một đơn vị dịch duy nhất
  • Mọi struct đều được typedef
    • Bỏ từ khóa struct giúp mã dễ đọc hơn
    • Với struct đệ quy, đặt forward declaration ngay phía trên và dùng tên ngắn cho field
  • Mọi hàm trừ entry point đều được khai báo static
    • Vì giả định biên dịch theo một đơn vị dịch duy nhất
  • Nhờ tên kiểu ngắn, bỏ const, và bỏ struct, có thể đặt kiểu trả về và tên hàm thoải mái trên cùng một dòng
  • Từng có thời gian viết tên kiểu bằng chữ hoa, nhưng cuối cùng đã bỏ

Chuỗi dùng s8 thay vì kết thúc bằng null

  • Một trong những thay đổi hiệu quả nhất là từ chối hoàn toàn chuỗi kết thúc bằng null và dùng kiểu chuỗi s8datalen
  • Cấu trúc s8:
    • u8 *data
    • size len
  • Macro s8(s) dùng để bọc string literal C thành chuỗi s8
  • s8 được truyền theo giá trị và trả về theo giá trị như một fat pointer
  • s8 cũng tiện dùng làm tiền tố cho hàm
    • Vì các tên kiểu str là tên dành riêng
    • Ví dụ: s8span, s8equals, s8compare, s8hash, s8trim, s8clone
  • Khi so sánh với literal, dùng dạng như s8equals(tagname, s8("body"))
  • Cũng từng thử cách dùng flexible array member để gộp kích thước và mảng vào một allocation, nhưng thấy tính kém linh hoạt lớn hơn lợi ích
  • Đã từng nghĩ rằng chương trình đơn giản thì không cần kiểu chuỗi, nhưng nhìn chung đó là nhận định sai
  • Cũng dùng s16 làm kiểu hỗ trợ UTF-16
    • c16 *datasize len
    • Vẫn chưa hoàn toàn chắc chắn về cách thêm tiền tố u cho literal trong macro

Trả về struct và cách khởi tạo

  • Ưu tiên trả về struct hơn tham số out
    • Thực chất đây là cách trả về nhiều giá trị, chỉ là không có destructuring
  • Ví dụ i32parse(s8) trả về cùng lúc value là kết quả parse và ok là trạng thái
  • Chi phí sao chép thêm được xem là không phải vấn đề lớn trong thực tế
    • Vì calling convention có thể chuyển nó thành tham số out restrict ẩn
    • Hoặc nếu được inline thì overhead của giá trị trả về gần như không đáng kể
  • Cách này làm giảm cám dỗ dùng tín hiệu in-band như trả về null đặc biệt để biểu thị lỗi
  • Ưu tiên mẫu tạo giá trị trả về đã khởi tạo 0 ở đầu hàm rồi dùng cho mọi return
    • Khi lỗi thì trả về ngay ở trạng thái đã khởi tạo 0
    • Trên nhánh thành công thì đặt ok thành true ngay trước khi trả về
  • Ngoài dữ liệu tĩnh và các macro s8·s16, cũng giảm dùng initializer
    • Cả designated initializer cũng tránh, thay vào đó khởi tạo bằng câu lệnh gán
  • Khởi tạo bằng gán dễ đọc hơn và giữa từng phép gán có sequence point, nên cho thứ tự tường minh
  • Với các khởi tạo mà thứ tự gọi ảnh hưởng đến kết quả, như hàm sinh số ngẫu nhiên, không cần phải suy nghĩ về các trường hợp giá trị có thể xảy ra

Khai báo Win32 và inline assembly

  • Thích __attribute hơn __attribute__
    • Hậu tố __ phía sau bị xem là quá mức và không cần thiết
  • Trong lập trình hệ thống Win32, không include windows.h mà tự viết các prototype cần thiết
    • Vì thông thường số lượng khai báo và định nghĩa cần dùng không nhiều
    • Giảm thời gian build và làm namespace đỡ lộn xộn hơn
    • Đồng thời khớp gọn gàng hơn với các kiểu tự định nghĩa như u32, b32, uptr thay cho DWORD, BOOL, ULONG_PTR
  • Ví dụ khai báo Win32 dùng macro W32(r) __declspec(dllimport) r __stdcall
    • Tự khai báo các hàm như ExitProcess, GetStdHandle, VirtualAlloc, WriteConsoleA, WriteConsoleW
  • Trong inline assembly, xem cặp ngoặc ngoài như dấu ngoặc nhọn
    • Đặt khoảng trắng trước ngoặc mở như với if
    • Mỗi dòng constraint bắt đầu bằng dấu hai chấm
  • Một ví dụ nhỏ để xem phong cách đã nêu là wordhist.c
  • Một ví dụ lớn hơn là asmint.c, hiện thực một ngôn ngữ lập trình tối giản

1 bình luận

 
GN⁺ 2023-10-10
Các ý kiến trên Hacker News
  • Có vẻ như với #define sizeof(x) (size)sizeof(x), tác giả cho rằng không cần ngoặc ngoài, nhưng có một ngoại lệ rất nhỏ
    Ép kiểu có độ ưu tiên cao hơn phép nhân, nên sizeof(x) * 3 hoạt động an toàn thành (size)sizeof(x) * 3
    Tuy nhiên, trong (size)sizeof(x)[y], truy cập chỉ số mảng được áp dụng trước ép kiểu, nên nó trở thành (size)(sizeof(x)[y]) chứ không phải ((size)sizeof(x))[y]
    Trong mã thực tế sẽ không có chuyện truy cập chỉ số trên sizeof(x), nhưng C cho phép integer[pointer] có cùng nghĩa với pointer[integer], nên macro này có thể biên dịch được nhưng hoạt động sai vì thiếu ngoặc
    Về bản chất hơn, cũng khó đồng ý với lập luận rằng signed size tốt hơn. Tác giả nói unsigned size là nguồn gốc của lỗi, nhưng đoạn mã được đưa ra cũng có lỗi làm hỏng bộ nhớ nếu count là số âm
    Với số nguyên không dấu, số lượng âm không thể được biểu diễn; nếu tràn số xảy ra thì nó sẽ thành một số dương rất lớn và bị các kiểm tra hiện có bắt được. Cá nhân tôi thích dùng số nguyên không dấu, nhưng nếu có thể thì bọc bằng wrapper kiểm tra phạm vi để dừng khi tràn số

    • Tôi lại thích ngữ nghĩa của _Bool
      Vì có thể tách một biểu thức hoạt động tốt trong if (flags & FLAG_ALLOCATED) thành biến boolean như _Bool need_free = flags & FLAG_ALLOCATED;
      Khi được đặt, flags & FLAG_ALLOCATED có thể không phải là 1 mà là một giá trị khác 0 bất kỳ; _Bool sẽ chuẩn hóa nó thành 1. Nếu nhận bằng int, if (need_free) sẽ qua, nhưng if (need_free == true) có thể thất bại
      Cũng có nhược điểm. Nếu trong lúc refactor bỏ sót việc chuyển đổi ngầm sang _Bool vốn đang làm một việc hữu ích, mã có thể trở thành sai như if ((flags & FLAG_ALLOCATED) == true)
      Ngoài ra, khi đọc struct từ đĩa hoặc điền các byte tùy ý vào, nếu trường _Bool không phải 0 hoặc 1 thì có nguy cơ dẫn tới hành vi không xác định
    • Thật ra (size)(sizeof(x)[y]) cũng sẽ gây bất ngờ với nhiều người, nhưng nó tương đương (size)(sizeof ((x)[y]))
      sizeof không phải là hàm mà là toán tử một ngôi, và truy cập chỉ số cùng lời gọi hàm có độ ưu tiên cao hơn sizeof. Vì vậy tôi thích đặt khoảng trắng sau sizeof, và chỉ dùng ngoặc cho toán hạng khi cần
      https://en.cppreference.com/w/c/language/operator_precedence
      Nếu muốn viết macro cho đúng thì sẽ là #define sizeof(x) ((size)(sizeof (x)))
    • Bắt lỗi hay đấy. Bài học là nếu định nghĩa macro không mở rộng thành chỉ một token duy nhất thì luôn phải bọc bằng ngoặc. Quy tắc độ ưu tiên của C thật sự phức tạp
  • Việc định nghĩa kiểu riêng có vẻ hơi đi quá xa
    Ngay cả người đã quen với các kiểu của C cũng phải học thêm một hệ thống đặc thù riêng để hiểu một chương trình. Việc ghi rõ kích thước là hợp lý, nên dùng uint32_t thay vì uint thì tôi hiểu được
    Những kiểu này nên được định nghĩa trong header phù hợp, và cũng có thể tôi sai vì đã lâu không dùng C

    • Trên thực tế, int của C là 32 bit
      Không phải trên mục tiêu 16 bit, nhưng bạn có thật sự định port một chương trình 5MB sang 16 bit không? Những lo lắng như vậy thường không đáng
      Vấn đề là long. Trên một số máy là 32 bit, trên một số máy là 64 bit nên gây nhầm lẫn. May là long long luôn là 64 bit, nên chỉ cần bỏ long là được
      char 8 bit, short 16 bit, int 32 bit, long long 64 bit là xong. Trong C, người ta đã lãng phí vô tận thời gian vào kích thước của int
    • Dù sao tác giả cũng đã giới hạn rằng đây là phong cách code cá nhân. Thành thật mà nói, các kiểu chuẩn quá dài dòng, và tôi nghĩ giá mà danh sách ngắn gọn mà người này đề xuất đã được chấp nhận từ trước thì tốt
    • Nói hơi đùa một chút, một phần đáng kể của lập trình là xử lý hệ thống kiểu của người khác
      Với người thường xuyên dùng C, các viết tắt ở đây khá quen thuộc, và so với một hệ thống kiểu tùy biến thì nó khá thanh nhã. Làm tôi liên tưởng đến Rust
    • Những kiểu đó có trong stdint.h
      Tôi luôn ngạc nhiên khi thấy nhiều dự án vất vả tạo lại file này
      Dịch lại các kiểu chuẩn sang tên riêng của mình gây phiền cho người đọc. Trước đây tôi từng hỏi trong một dự án C++ vì sao dùng hàng loạt typedef cho collection, reference và object phức hợp, thì được trả lời là như vậy dễ hiểu hơn
      Sau đó tôi thấy cạnh màn hình của người đó dán một cheatsheet typedef
    • Việc tự định nghĩa kiểu số nguyên có ý nghĩa trên nền tảng bị giới hạn tài nguyên
      Thường thấy các kiểu như dim_t, tùy mục đích mà là 32 bit hoặc 64 bit. Ngay cả trên nền tảng 64 bit, trong cấu trúc nén con trỏ người ta vẫn thường dùng số nguyên 32 bit
      Ví dụ, nếu tự cấp phát heap và chỉ lưu offset 32 bit, với tải công việc dưới 4GB thì mức dùng bộ nhớ giảm một nửa, tính cục bộ cache cũng tốt hơn nên hiệu năng được cải thiện
  • Việc vứt bỏ các quy ước đã được xác lập của C chỉ vì sở thích cá nhân có vẻ hơi quá
    Dùng u8, i32 thay cho uint8_t hay int32_t có thể bớt được vài ký tự, nhưng có thể khiến người khác đọc code bị bối rối
    Việc dùng kiểu chuỗi tùy chỉnh thay cho chuỗi kết thúc bằng null cũng tạo cảm giác làm tăng độ khó khi cộng tác, nếu nghĩ đến việc C được tạo ra xoay quanh loại chuỗi như vậy
    Tự viết prototype của Win32 API thay vì include windows.h có thể giảm thời gian biên dịch, nhưng giống như bỏ đường cao tốc đã được bảo trì tốt để đi đường rừng. Phần lớn những điều đó trông giống sở thích cá nhân hơn là code C dễ xử lý cho mọi người

    • u8 hay i32 không phải để tiết kiệm số lần gõ phím, mà là để giảm gánh nặng cảm giác khi đọc
      Lập luận “số lần nhấn phím” luôn xuất hiện trong cuộc tranh luận dài dòng và súc tích có khiếm khuyết lớn. Niềm tin rằng sự súc tích chỉ tốt cho gõ nhanh, còn sự dài dòng thì luôn tốt hơn cho việc đọc là sai
      Dài dòng cũng có lợi thế cho việc đọc hiểu, nhưng súc tích cũng có lợi thế, và không bên nào là người thắng rõ ràng. Chỉ là những đánh đổi khác nhau
    • Những tên như u16 được dùng nhiều, và khả năng làm lập trình viên bối rối là thấp
      Điểm thực sự sụp đổ là khi hai chương trình khác nhau mỗi bên định nghĩa u16 riêng rồi phơi nó ra trong file header, và một chương trình thứ ba include cả hai header cùng lúc
      Kiểu thư viện có namespace sẽ thành dạng như libname_u32, và đến mức đó thì thay vì dùng tiền tố libname_, người ta lại muốn dùng luôn uint32_t
    • Khả năng một lập trình viên C có năng lực nhìn thấy u8 hay i32 rồi bị nhầm lẫn nhiều lắm cũng chỉ là lý thuyết, và trông hơi giống lập luận người rơm
      Có thể họ sẽ khó chịu, nhưng không phải là nhầm lẫn. Như Rich Hickey từng nói, trước khi học cách đọc thì mọi thứ đều khó đọc
  • Có ý kiến nói dùng boolean 32-bit có thể trông như lãng phí bộ nhớ với người mới, vậy chắc tôi cũng là người mới rồi
    Tôi đã nghe vài trường hợp nó không tệ hơn bool 8-bit, nhưng không thấy trường hợp nào thực sự tốt hơn. Nếu có các boolean nằm cạnh nhau trong struct, hoặc biến boolean của hàm bị đẩy khỏi thanh ghi xuống stack, thì vẫn lãng phí bộ nhớ
    Dù chỉ vài byte, tôi không hiểu vì sao lại cố tình làm bi quan hóa. Dùng kích thước lớn hơn thì được gì?

    • Tùy hoàn toàn vào kiến trúc và CPU, nhưng theo kinh nghiệm trước đây, một ví dụ rõ ràng là tác vụ xử lý số
      Cấu trúc khi đó là có giá trị điều kiện ở đầu struct theo chu kỳ, phía sau là 512, 1024, 2048 giá trị mẫu; một junior muốn tiết kiệm không gian nên đã pack struct và biến giá trị điều kiện thành 8-bit 1 byte
      Đoạn code “cải tiến” đó làm thông lượng trên chip Intel giảm khoảng 10 lần, và trên kiến trúc SPARC RISC thì gây BUS ERROR
      Khi pack phần header của struct, mảng dữ liệu bị mất căn chỉnh; Intel phải âm thầm lấy hai word 32-bit rồi ghép lại, còn SPARC thì nổi giận đúng nghĩa với dữ liệu không căn chỉnh
      Nếu là tính toán pipeline coi trọng thông lượng chứ không phải lưu file dài hạn, đôi khi căn dữ liệu theo căn chỉnh kiến trúc tốt hơn là pack theo kiểu “tiết kiệm không gian”
    • Thường thì tối ưu hóa dễ nhất là padding các trường struct cho khớp biên 32-bit
      Hầu như mọi compiler đều làm điều này, nên chỉ cần tìm “struct alignment/padding”. Nếu compiler dù sao cũng sẽ để khoảng trống, thì tự dùng phần bộ nhớ đó còn hơn; nếu không, có thể mất hiệu năng
      Chính xác hơn, mỗi trường phải nằm ở địa chỉ chia hết cho kích thước của chính nó hoặc kích thước wordline, và toàn bộ struct cũng phải được padding thành bội số của kích thước trường lớn nhất. Trên thực tế, thường có nghĩa là căn chỉnh 32-bit
      Tham khảo: http://www.catb.org/esr/structure-packing/
    • Nếu dùng kiểu bool thật, sanitizer sẽ cảnh báo khi giá trị không phải là 0 tương ứng false hoặc 1 tương ứng true
    • Kiến trúc máy tính phần lớn được tối ưu cho truy cập căn chỉnh từ 32-bit trở lên. Thứ nhận được thường là, nhưng không phải luôn luôn, hiệu năng
    • Tôi tò mò ví dụ về một hàm mà biến boolean bị đẩy xuống stack và 3 byte đó trở nên quan trọng là gì
  • Tôi không đồng ý với lập luận về trả về struct và tham số đầu ra
    Nó khiến việc kết hợp các hàm có thể trả lỗi khó hơn nhiều, và kiểu dữ liệu phình ra khắp nơi. Thực tế gần như mọi hàm đều có thể thất bại, nên đặc biệt nếu xử lý cả thiếu bộ nhớ thì phong cách trả lỗi có thể dự đoán quan trọng hơn

    • Gần như không ai xử lý thiếu bộ nhớ
      Việc đó rất khó và lợi ích cũng gần như không có. Đến điểm đó thì sẽ phát sinh những vấn đề khác hẳn, lớn hơn nhiều so với lựa chọn phong cách lập trình
    • Nếu có thể unpack struct một cách có ý nghĩa thì có lẽ đã có được ngữ nghĩa errno và tham số đầu ra thông thường, nhưng C không có
      Dù vậy, việc kết hợp các giá trị tùy chọn trong C luôn hơi đau khổ. Nếu không dùng exception, ở mỗi ngôn ngữ, exception và monad có vẻ như là hai lựa chọn lớn, nhưng cả hai đều không hợp với C và cũng không hợp với triết lý của phần lớn lập trình viên C
      Với lời gọi một-đối-một đơn giản có thể thử macro, nhưng có giới hạn. Dù C++ có kinh khủng, C++ optional vẫn dễ chịu hơn if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... }
    • Trả về option hoặc sum type là đúng, nhưng trong C thì viết thật sự rườm rà
      Mẫu thêm if (thing(...)) goto fail vào mọi lời gọi hàm cũng không có vẻ tuyệt lắm, nhưng phía Go có vẻ thích
      Hoặc có thread_local mylibrary_errno, cách này trong nội bộ thư viện thực ra có thể đúng, rồi ở ranh giới thì chuyển thành giá trị trả về dạng enum
  • Từ đoạn “signed sizes are the way” là tôi chỉ muốn nói đọc tới đó là đủ rồi
    signed size là một rò rỉ trừu tượng rất đáng kinh ngạc và là cách làm dễ dẫn tới thảm họa
    Cũng khó chấp nhận ý rằng const không có vai trò thực dụng và chưa từng giúp bắt lỗi. Người ta thường nhầm lẫn giữa bộ đệm đầu vào và bộ đệm đầu ra, và const làm lộ điều đó ngay lập tức
    Việc bảo mọi hàm ngoài điểm vào đều để static cũng có thể khiến khi debug không tìm được biến hay hàm rồi nguyền rủa tác giả
    Ưu tiên trả về struct rất dễ vô tình trả về stack pointer và mở ra lỗ hổng bảo mật lớn. Nếu truyền buffer đầu ra thì ngữ nghĩa sở hữu sẽ rõ ràng hơn
    Lời khuyên này có thể tạm ổn với người chủ yếu viết mã hệ thống 64-bit, nhưng trong mảng embedded 32-bit thì có thể nhanh chóng trở nên rắc rối

    • Bjarne Stroustrup đã viết một ghi chú chi tiết bênh vực signed size
      https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
    • Những người ghét const sẽ không bao giờ hài lòng, nên cứ dùng const ở chỗ cần, lan truyền nó tới mức cần thiết rồi bỏ qua phàn nàn là được
      Họ bỏ ra thì mình thêm lại. Lúc nào phía đó cũng mệt trước. Tôi đã làm vậy 25 năm rồi và vẫn còn ở đây
      static có thể tùy thuộc vào công cụ. Khoảng 15 năm trước tôi đã chuyển sang mặc định dùng static và dùng size_t toàn diện, tới giờ vẫn chưa gặp vấn đề
  • Điều thú vị là trải nghiệm của tôi lại dẫn tới hướng khác
    https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
    Bài viết xoay quanh D, nhưng nguyên tắc cũng áp dụng cho C

    • Đọc khá thú vị
      Phần chuyển biểu thức điều kiện vào bên trong doX()doZ() rất đáng chú ý. Tôi không biết có phải lúc nào cũng đúng không; nó còn tùy abstraction nằm ở đâu và mental model về đoạn mã
      Ví dụ deleteRecords(); không tốt hơn if let x = deadRecords() deleteRecords(x);. Vế sau trông rối hơn, nhưng có giá trị ở chỗ cho thấy ngay từ đầu rằng đây không phải xóa mà là tỉa bớt
      Nếu đổi tên hàm khôn ngoan thành kiểu pruneDeadProjects() thì ổn, nhưng chỉ chuyển điều kiện vào trong hàm có thể khiến ngữ cảnh trở nên nguy hiểm và thành một abstraction bị rò rỉ
  • Tôi ủng hộ việc dùng typedef cho mọi struct vì nó giúp ngắn gọn
    Theo tôi có thể dùng typedef rộng rãi. Tuy nhiên chỉ nên typedef chính đối tượng, không typedef con trỏ. Khi cần con trỏ thì lúc nào cũng có thể dùng (type *)
    Đặc biệt với function pointer, không nên typedef function pointer mà nên typedef chính hàm. Khi đó cũng có thể dùng typedef đó trong khai báo hàm để được kiểm tra kiểu tham số, và khi đổi signature của hàm thì không cần sửa toàn bộ các khai báo
    Phần lớn codebase C làm sai chỗ này: typedef function pointer, rồi vẫn phải tự viết khai báo hàm khớp với định nghĩa con trỏ đó
    Tôi vẫn chưa bị thuyết phục bởi cách dùng struct làm kiểu trả về. Tôi thích để mã lỗi dạng số làm giá trị trả về, còn các giá trị trả về khác thì nhận qua tham số đầu ra hơn

    • Với struct opaque, tôi thích dùng typedef để mô phỏng class có toàn bộ field private, còn với cấu trúc dữ liệu đơn giản thì dùng struct
      Class nên chỉ được truy cập qua hàm, còn struct thì phải có thể truy cập trực tiếp
      Điều này nhìn chung gần với quy ước chuẩn C/POSIX. Ví dụ là khác biệt giữa pthread_tstruct stat
    • Tôi đồng ý với việc không typedef chính con trỏ
      Những thứ như SDL_net làm đúng kiểu đó nên tôi không thích. Thực ra là con trỏ nhưng lại typedef như kiểu giá trị
      Tôi hiểu ý đồ, nhưng đó là cách làm khá khó chịu
  • Nhiều phần trong bài này khá thuyết phục
    Tôi đã bắt đầu viết bare-metal OS cho Arm64, dù vẫn ở giai đoạn đầu nhưng đang làm những việc tương tự. Tôi dùng chuỗi Pascal và cũng đổi tên kiểu. Chỉ là theo phong cách int8 chứ không phải i8
    Vì đã nhanh chóng quyết định rằng mình không định port phần mềm thực tế nào, nên không cần theo các hàm hay quy ước của thư viện C chuẩn. Nhờ vậy có thể thử nghiệm tự do hơn
    C là ngôn ngữ cũ đến mức gánh nặng từ thời từng byte đều quý giá vẫn còn lưu lại cả trong tên hàm. Thoát khỏi điều đó thật dễ chịu, và nội dung bài này cùng nhiều đổi tên nhỏ trông như một đợt dọn dẹp khá gọn gàng

    • Nói không cần theo các hàm hay quy ước của thư viện C chuẩn có nghĩa là chỉ làm như sở thích cá nhân, chứ không phải thứ lớn và chuyên nghiệp như GNU à?
    • Tôi chưa từng nghĩ tới chuyện tiết kiệm byte cả trong symbol
  • typedef float f32;, typedef double f64; trông như một bước đệm nguy hiểm, vì giả định rằng float là 32 bit và double là 64 bit
    OpenCV định nghĩa float16_t, CUDA triển khai số thực dấu phẩy động bán chính xác, còn các vi điều khiển có thể triển khai mỗi nơi một kiểu
    C++23 đưa vào các kiểu số thực dấu phẩy động có độ rộng cố định, nhưng tôi không biết cách nào để bắt buộc điều đó trong C. Có vẻ tốt hơn là đặt macro để kiểm tra tại thời điểm biên dịch xem có mất dữ liệu hay không
    Nhìn chung, như những người khác đã nói, vì khả năng đọc, có lẽ nên để một số thứ ở dạng mặc định, dù không ngắn gọn
    [0] https://docs.opencv.org/4.x/df/dc9/classcv_1_1float16__t.htm...
    [1] https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH...
    [2] https://en.cppreference.com/w/cpp/types/floating-point