Phong cách viết mã C cá nhân ở thời điểm cuối năm 2023
(nullprogram.com)- 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ỏconstvàstruct, 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
s8códatavàlen; trong môi trường Win32·UTF-16 thì dùng thêmc16vàs16 - 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
oktạ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í dụ:
- 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
ihơnssđược dành lại để dùng cho tên kiểu chuỗi
- Với kiểu kích thước, dùng
sizethay vìisize- Vì xem signed size là mặc định quan trọng hơn
usizechủ yếu chỉ có phạm vi hẹp khi tương tác với giao diện bên ngoài
b32thể 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
- Dùng kích thước từ tự nhiên thay cho
c16là 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
- Nếu dựa trên
u8được dùng cho octet và chủ yếu cho dữ liệu UTF-8, cònbyteđượ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_tcũng bị xem là lãng phí không cần thiết
- Những tên kiểu dài như
- 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
- Vì người đọc cần cả
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í dụ:
- 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·fieldnew - Vì nếu không ở dạng gọi hàm thì nó sẽ không được mở rộng thành macro
- Có thể đồng thời có macro
- Macro
assertcho GCC và Clang dùng dạngwhile (!(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
libubsancung 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-trapvà 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
constcho 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ỏ
constlà 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
constnhư gợi ý để đặt bảng tĩnh vào vùng nhớ chỉ đọc gần mã- Khi cần thì sẽ bỏ
constbằng cast
- Khi cần thì sẽ bỏ
- 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
restrictkhi 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
structgiú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
- Bỏ từ khóa
- 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
s8códatavàlen - Cấu trúc
s8:u8 *datasize len
- Macro
s8(s)dùng để bọc string literal C thành chuỗis8 s8được truyền theo giá trị và trả về theo giá trị như một fat pointers8cũng tiện dùng làm tiền tố cho hàm- Vì các tên kiểu
strlà tên dành riêng - Ví dụ:
s8span,s8equals,s8compare,s8hash,s8trim,s8clone
- Vì các tên kiểu
- 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
s16làm kiểu hỗ trợ UTF-16- Có
c16 *datavàsize len - Vẫn chưa hoàn toàn chắc chắn về cách thêm tiền tố
ucho literal trong macro
- Có
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úcvaluelà kết quả parse vàoklà 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ể
- Vì calling convention có thể chuyển nó thành tham số out
- 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
okthà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
__attributehơn__attribute__- Hậu tố
__phía sau bị xem là quá mức và không cần thiết
- Hậu tố
- Trong lập trình hệ thống Win32, không include
windows.hmà 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,uptrthay choDWORD,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
- Tự khai báo các hàm như
- 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
- Đặt khoảng trắng trước ngoặc mở như với
- 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
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) * 3hoạt động an toàn thành(size)sizeof(x) * 3Tuy 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épinteger[pointer]có cùng nghĩa vớipointer[integer], nên macro này có thể biên dịch được nhưng hoạt động sai vì thiếu ngoặcVề 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
countlà số âmVớ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ố
_BoolVì 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_ALLOCATEDcó thể không phải là 1 mà là một giá trị khác 0 bất kỳ;_Boolsẽ chuẩn hóa nó thành 1. Nếu nhận bằngint,if (need_free)sẽ qua, nhưngif (need_free == true)có thể thất bạiCũng có nhược điểm. Nếu trong lúc refactor bỏ sót việc chuyển đổi ngầm sang
_Boolvố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
_Boolkhông phải 0 hoặc 1 thì có nguy cơ dẫn tới hành vi không xác định(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]))sizeofkhô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ơnsizeof. Vì vậy tôi thích đặt khoảng trắng sausizeof, và chỉ dùng ngoặc cho toán hạng khi cầnhttps://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)))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_tthay vìuintthì tôi hiểu đượcNhữ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
intcủa C là 32 bitKhô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 longluôn là 64 bit, nên chỉ cần bỏlonglà đượcchar8 bit,short16 bit,int32 bit,long long64 bit là xong. Trong C, người ta đã lãng phí vô tận thời gian vào kích thước củaintVớ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
stdint.hTô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
typedefcho collection, reference và object phức hợp, thì được trả lời là như vậy dễ hiểu hơnSau đó tôi thấy cạnh màn hình của người đó dán một cheatsheet typedef
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 bitVí 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,i32thay chouint8_thayint32_tcó thể bớt được vài ký tự, nhưng có thể khiến người khác đọc code bị bối rốiViệ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.hcó 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ườiu8hayi32khô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 đọcLậ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
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
u16riê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úcKiể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ônuint32_tu8hayi32rồ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ơmCó 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ì?
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”
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/
boolthật, sanitizer sẽ cảnh báo khi giá trị không phải là 0 tương ứngfalsehoặc 1 tương ứngtrueTô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
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
errnovà 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) { ... }Mẫu thêm
if (thing(...)) goto failvà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íchHoặ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 enumTừ đ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
constkhô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àconstlàm lộ điều đó ngay lập tứcViệc bảo mọi hàm ngoài điểm vào đều để
staticcũ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
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
constsẽ không bao giờ hài lòng, nên cứ dùngconstở chỗ cần, lan truyền nó tới mức cần thiết rồi bỏ qua phàn nàn là đượcHọ 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
staticcó 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ùngstaticvà dùngsize_ttoà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
Phần chuyển biểu thức điều kiện vào bên trong
doX()và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ơnif 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ớtNế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
typedefcho mọi struct vì nó giúp ngắn gọnTheo tôi có thể dùng
typedefrộng rãi. Tuy nhiên chỉ nêntypedefchính đối tượng, khôngtypedefcon 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
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ùngstructClass 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_tvàstruct stattypedefchí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
typedefnhư 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
int8chứ không phảii8Vì đã 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
typedef float f32;,typedef double f64;trông như một bước đệm nguy hiểm, vì giả định rằngfloatlà 32 bit vàdoublelà 64 bitOpenCV đị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ểuC++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
_Floattypedef _Float32 f32;typedef _Float64 f64;https://gcc.gnu.org/onlinedocs/gcc/Floating-Types.html