Bài 54.226 phút đọc
Macro biến đối số, nối và chuỗi hóa
Sau bài này bạn sẽ làm được
- Viết macro nhận số đối số thay đổi
- Xử lý trường hợp không có đối số nào
- Dùng # để biến biểu thức thành chuỗi
- Dùng ## và biết vì sao cần hai tầng macro
Bốn công cụ: __VA_ARGS__ để nhận số đối số thay đổi, __VA_OPT__ để xử lý trường hợp không có đối số nào, # để biến biểu thức thành chuỗi, và ## để nối tên. Ba cái sau đều có cạm bẫy riêng.
#__VA_ARGS__
Cú pháp
#define GHI_LOG(dinh_dang, ...) \
printf("[%s:%d] " dinh_dang "\n", __FILE__, __LINE__, __VA_ARGS__)
GHI_LOG("gia tri = %d", x);
GHI_LOG("hai gia tri: %d %d", x, y);
/* Bung ra:
printf("[%s:%d] " "gia tri = %d" "\n", "a.c", 12, x);
Ba chuoi lien nhau duoc noi lai luc dich, nen ket qua la:
printf("[%s:%d] gia tri = %d\n", "a.c", 12, x);
Ky thuat noi chuoi hang nay la co so cua moi macro ghi nhat ky. */#Vấn đề không có đối số nào
Cạm bẫy kinh điển
#define GHI_LOG(dinh_dang, ...) \
printf(dinh_dang "\n", __VA_ARGS__)
GHI_LOG("bat dau");
/* Bung ra:
printf("bat dau" "\n", );
^ dau phay THUA
error: expected expression before ')' token */| Cách chữa | Chuẩn | Nhược điểm |
|---|---|---|
| ##__VA_ARGS__ | Mở rộng GNU | Không chuẩn, MSVC cũ không hỗ trợ |
| __VA_OPT__(,) | C23, và GCC 8 trở lên đã có | Cần trình biên dịch mới |
| Bắt đối số đầu vào ... | Chuẩn C99 | Cần một thủ thuật, xem dưới |
| Luôn có ít nhất một đối số | Chuẩn C99 | Người dùng phải nhớ |
Bốn cách, viết đủ
/* 1. ##__VA_ARGS__ cua GNU: dau ## XOA dau phay truoc no
neu __VA_ARGS__ rong */
#define GHI_LOG(dd, ...) printf(dd "\n", ##__VA_ARGS__)
GHI_LOG("bat dau"); /* printf("bat dau" "\n"); dung */
GHI_LOG("x = %d", x); /* printf("x = %d" "\n", x); dung */
/* 2. __VA_OPT__ cua C23: chi chen noi dung neu __VA_ARGS__ KHONG rong */
#define GHI_LOG(dd, ...) printf(dd "\n" __VA_OPT__(,) __VA_ARGS__)
GHI_LOG("bat dau"); /* khong chen dau phay */
GHI_LOG("x = %d", x); /* chen dau phay */
/* 3. Chuan C99: bat dinh dang vao trong ... roi tach ra */
#define GHI_LOG(...) ghi_log_that(__FILE__, __LINE__, __VA_ARGS__)
static void ghi_log_that(const char *tep, int dong, const char *dd, ...) {
va_list ap;
printf("[%s:%d] ", tep, dong);
va_start(ap, dd);
vprintf(dd, ap);
va_end(ap);
putchar('\n');
}
GHI_LOG("bat dau"); /* dung: dinh dang la doi so dau cua ... */
GHI_LOG("x = %d", x); /* dung */
/* Cach 3 la cach CHUAN va thuong la tot nhat:
- khong dung mo rong nao
- chuyen viec noi chuoi sang cho ham xu ly
- va ban co the them muc do log, mau sac, ghi ra tep, tat bat */
/* 4. Bat nguoi dung luon truyen it nhat mot doi so */
#define GHI_LOG(dd, ...) printf(dd "\n", __VA_ARGS__)
GHI_LOG("%s", "bat dau"); /* nguoi dung tu them */terminal
# GCC 8.1 hỗ trợ __VA_OPT__ với -std=c2x, và ##__VA_ARGS__ luôn có
gcc -std=c11 -Wall -Wpedantic -c log.c
log.c:4:41: warning: ISO C++11 requires at least one argument for the "..." in a variadic macro [-Wpedantic]
#Toán tử #
#define CHUOI(x) #x
CHUOI(hello) /* "hello" */
CHUOI(1 + 2) /* "1 + 2" */
CHUOI(a, b) /* error: macro "CHUOI" passed 2 arguments */
/* Toan tu # bien MOT tham so thanh mot chuoi hang.
No giu nguyen van ban ban viet, ke ca khoang trang o giua,
nhung gop nhieu khoang trang lien tiep thanh mot. */
CHUOI( a + b ) /* "a + b" */terminal
# Chuỗi hóa giữ nguyên dấu ngoặc kép và dấu chéo ngược trong chuỗi con
gcc -E -P chuoi.c
int main(void) {
puts("printf(\"C:\\tmp\")");
return 0;
}#Toán tử ##
#define NOI(a, b) a##b
NOI(x, 1) /* x1 */
NOI(ham_, doc) /* ham_doc */
NOI(1, 2) /* 12, mot hang so */
/* Toan tu ## noi hai token thanh MOT token.
Ket qua phai la mot token hop le, neu khong thi UB. */
NOI(1, x) /* error: invalid suffix "x" on integer constant
vi "1x" khong phai token hop le */Cạm bẫy: ## với đối số rỗng
#define F(a, b) a##b
F(x, ) /* "x" */
F(, y) /* "y" */
F(, ) /* rong, hop le */
/* Chuan noi: neu mot toan hang cua ## la mot chuoi token rong
thi ket qua la toan hang kia.
Do la co so cho thu thuat ##__VA_ARGS__ o muc tren:
khi __VA_ARGS__ rong, "," ## rong -> dau phay bi xoa.
Nhung chu y do la MO RONG cua GNU: theo chuan, ## voi mot ben la
dau phay va ben kia la __VA_ARGS__ rong KHONG duoc dinh nghia.
C23 giai quyet bang __VA_OPT__ mot cach chinh thuc. */#Vì sao cần hai tầng macro
Quy tắc bung macro, rút gọn
/* Bo tien xu ly xu ly mot loi goi macro theo ba buoc:
Buoc 1: bung cac THAM SO, TRU nhung tham so di kem # hoac ##
Buoc 2: thay tham so vao than macro,
ap dung # va ##
Buoc 3: bung lai KET QUA mot lan nua
Nen: */
#define A 100
#define S1(x) #x /* x di kem #, KHONG bung o buoc 1 */
#define S2(x) S1(x) /* x KHONG di kem gi, bung o buoc 1 */
S1(A) /* buoc 1: khong bung A -> buoc 2: #A -> "A" */
S2(A) /* buoc 1: A bung thanh 100 -> buoc 2: S1(100)
-> buoc 3: bung S1(100) -> "100" */terminal
# Xem sự khác biệt bằng gcc -E
gcc -E -P hai_tang.c
const char *a = "PHIEN_BAN"; const char *b = "3";
#Đếm số đối số
Thủ thuật đếm, dùng trong nhiều thư viện
/* Y tuong: day danh sach doi so vao mot danh sach co dinh,
roi doc phan tu o vi tri co dinh. */
#define DEM_DOI_SO(...) \
DEM_TRONG(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define DEM_TRONG(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N
DEM_DOI_SO(a) /* 1 */
DEM_DOI_SO(a, b) /* 2 */
DEM_DOI_SO(a, b, c) /* 3 */
/* Cach hoat dong voi DEM_DOI_SO(a, b, c):
DEM_TRONG(a, b, c, 8, 7, 6, 5, 4, 3, 2, 1, 0)
_1 _2 _3 _4 _5 _6 _7 _8 N ...
_1 = a, _2 = b, _3 = c, _4 = 8, _5 = 7, _6 = 6,
_7 = 5, _8 = 4, N = 3
-> tra ve 3
Cac so 8 xuong 0 dong vai tro "day len": cang nhieu doi so that
thi day cac so do sang phai, va N roi vao dung so ban muon. */Tự làm thử
- Viết macro ghi nhật ký và gọi nó không có đối số nào ngoài chuỗi.
- Sửa nó bằng cả ba cách:
##__VA_ARGS__,__VA_OPT__, và hàmva_list. - Chứng minh
#không mở rộng macro trong đối số, rồi sửa bằng hai tầng. - Dùng
##để sinh tên hàm cho ba kiểu khác nhau. - Cài macro đếm số đối số và kiểm tra với một tới tám đối số.
- Viết macro chọn hàm theo số đối số, rồi viết lại bằng ba hàm có tên khác nhau và so hai cách.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
__VA_ARGS__là một chuỗi token gồm cả dấu phẩy, không phải danh sách duyệt được.- Trường hợp không có đối số nào để lại một dấu phẩy thừa;
##__VA_ARGS__của GNU và__VA_OPT__của C23 đều chữa được, còn cách chuẩn là gọi vào hàmva_list. #và##đều không mở rộng macro trong đối số, và cách chữa cho cả hai là thêm một tầng macro.#thoát dấu nháy và dấu chéo ngược chỉ khi chúng nằm trong chuỗi con hoặc hằng ký tự của đối số.- Đếm đối số làm được nhưng thường không nên: ba hàm tên khác nhau rõ ràng hơn nhiều.