Bài 29.628 phút đọc
Closure trong C
Sau bài này bạn sẽ làm được
- Phân biệt hàm nặc danh với bao đóng
- Cài closure chuẩn bằng cặp con trỏ hàm và con trỏ ngữ cảnh
- Dùng compound literal để tạo ngữ cảnh ngay tại chỗ
- Nêu vì sao hàm lồng nhau của GCC không nên dùng
Nói ngay cho rõ: C từ C89 tới C23 không có lambda. Có đề xuất cho C2y đang được bàn nhưng chưa vào chuẩn. Bài này dạy các cách mô phỏng, và đánh giá thẳng thắn cách nào đáng dùng.
#Hai khái niệm hay bị lẫn
Hàm nặc danh
Hàm không có tên, viết ngay tại chỗ dùng. Chỉ là tiện lợi cú pháp.
Bao đóng
Hàm cộng với môi trường mà nó bắt được: những biến bên ngoài mà thân hàm tham chiếu tới. Đây mới là phần khó, và cũng là phần có giá trị thật.
/* Trong mot ngon ngu co closure: */
int he_so = 10;
auto nhan = [he_so](int x) { return x * he_so; }; /* C++ */
/* Ham nhan "bat" duoc bien he_so. Doi tuong nhan mang theo
ban sao cua he_so, va no song lau hon pham vi noi he_so
duoc khai bao.
Trong C, ban phai tu lam ca hai viec do:
1. goi gia tri bat duoc vao dau do
2. truyen cho do cho ham */#Cách một: con trỏ hàm cộng ngữ cảnh
Chuẩn C 100 phần trăm, khả chuyển tuyệt đối
/* Mot "closure" la mot cap: ham, va con tro toi moi truong */
typedef struct {
int (*goi)(void *ctx, int x);
void *ctx;
} HamInt;
static int ap_dung(HamInt f, int x) { return f.goi(f.ctx, x); }
void map_int(int *a, size_t n, HamInt f) {
for (size_t i = 0; i < n; ++i)
a[i] = f.goi(f.ctx, a[i]);
}
/* Moi truong bat duoc: mot struct do ban dinh nghia */
typedef struct { int n; } CtxNhan;
static int than_nhan(void *ctx, int x) {
return x * ((CtxNhan *)ctx)->n;
}
int main(void) {
int a[] = { 1, 2, 3, 4, 5 };
CtxNhan c = { .n = 10 };
HamInt nhan10 = { than_nhan, &c }; /* "lambda bat bien n" */
map_int(a, 5, nhan10);
for (int i = 0; i < 5; ++i) printf("%d ", a[i]);
return 0;
}terminal
gcc -std=c11 -Wall -Wextra -o closure closure.c && ./closure
10 20 30 40 50
Áp dụng từng phần: hàm trả về closure
/* Nhan f(a, b) va gia tri a, tra ve g(b) = f(a, b) */
typedef struct {
int (*goi)(void *ctx, int x);
void *ctx;
void (*huy)(void *ctx); /* de giai phong ngu canh */
} HamInt;
typedef struct { int a; int (*f)(int, int); } CtxApDung;
static int than_ap_dung(void *ctx, int b) {
CtxApDung *c = ctx;
return c->f(c->a, b);
}
static void huy_ap_dung(void *ctx) { free(ctx); }
HamInt ap_dung_tung_phan(int (*f)(int, int), int a) {
CtxApDung *c = malloc(sizeof *c);
if (!c) return (HamInt){ NULL, NULL, NULL };
c->a = a;
c->f = f;
return (HamInt){ than_ap_dung, c, huy_ap_dung };
}
/* Dung: */
static int cong(int a, int b) { return a + b; }
HamInt cong5 = ap_dung_tung_phan(cong, 5);
printf("%d\n", cong5.goi(cong5.ctx, 10)); /* 15 */
cong5.huy(cong5.ctx); /* dung quen */#Cách hai: compound literal
Compound literal
Cú pháp C99 tạo một đối tượng vô danh ngay trong biểu thức:
(KieuNao){ .truong = gia_tri }. Ở phạm vi khối nó có thời gian sống tự động, ở phạm vi tệp thì có thời gian sống tĩnh.#define CLOSURE(fn, ...) \
((HamInt){ (fn), &(CtxNhan){ __VA_ARGS__ } })
int a[] = { 1, 2, 3, 4, 5 };
map_int(a, 5, CLOSURE(than_nhan, .n = 10));
/* Bung ra thanh:
map_int(a, 5, (HamInt){ than_nhan, &(CtxNhan){ .n = 10 } });
Doi tuong CtxNhan vo danh duoc tao ngay tai cho, va no song
toi het KHOI LENH chua bieu thuc nay. Trong suot loi goi
map_int thi no con song, nen an toan. */#Ba cách không chuẩn
Hàm lồng nhau của GCC
/* MO RONG GCC, khong co trong chuan, Clang va MSVC khong ho tro */
int main(void) {
int a[] = { 1, 2, 3 };
int he_so = 10;
int nhan(int x) { return x * he_so; } /* bat duoc he_so */
ap_dung(a, 3, nhan);
return 0;
}Blocks của Clang
/* MO RONG CLANG: clang -fblocks, tren Linux can them -lBlocksRuntime */
#include <Block.h>
typedef int (^BlockInt)(int);
BlockInt tao_cong(int n) {
return Block_copy(^(int x) { return x + n; }); /* chep tu stack sang heap */
}
int main(void) {
__block int dem = 0; /* __block: cho phep block GHI vao */
void (^tang)(void) = ^{ dem++; };
tang(); tang();
printf("%d\n", dem); /* 2 */
BlockInt f = tao_cong(5);
printf("%d\n", f(10)); /* 15, closure song lau hon ham tao */
Block_release(f);
return 0;
}Macro LAMBDA bằng biểu thức khối
/* MO RONG GNU, chay tren GCC va Clang, KHONG chay tren MSVC */
#define LAMBDA(kieu, than) ({ kieu __fn__ than __fn__; })
int (*bp)(int) = LAMBDA(int, (int x) { return x * x; });
printf("%d\n", bp(7)); /* 49 */
qsort(a, n, sizeof a[0],
LAMBDA(int, (const void *p, const void *q) {
int x = *(const int *)p, y = *(const int *)q;
return (x > y) - (x < y);
}));#Bảng so sánh
| Cách | Chuẩn C | Bắt biến | Sống sau khi hàm cha trả về | Đánh giá |
|---|---|---|---|---|
| Con trỏ hàm cộng void *ctx | C89 | Có, bạn tự làm | Có, nếu ngữ cảnh còn sống | Dùng cái này |
| Compound literal cộng ctx | C99 | Có | Chỉ trong cùng khối | Gọn, dùng cho lambda tại chỗ |
| Hàm lồng của GCC | Không | Có, tự động | Không, treo ngay | Chỉ để đọc mã cũ |
| Blocks của Clang | Không | Có, tự động | Có, nhờ Block_copy | Hệ Apple |
| Macro LAMBDA | Không | Nguy hiểm | Không | Chỉ khi không bắt gì |
Tự làm thử
- Cài
HamIntvàmap_int, rồi viết ba closure bắt các biến khác nhau. - Viết
ap_dung_tung_phantrả về closure, dùng nó, và kiểm bằng Valgrind rằng không rò rỉ. - Cố ý trả về closure có ngữ cảnh là biến cục bộ, chạy dưới ASan, và đọc thông báo.
- Viết macro
CLOSURE2nhận cả kiểu ngữ cảnh và dùng nó với hai kiểu khác nhau. - Viết thư viện
functional.hcómap,filter,reduce, tất cả nhận cặp hàm và ngữ cảnh. - Viết hệ thống sự kiện có
dang_ky(su_kien, xu_ly, ctx)vàphat(su_kien, du_lieu), cho phép nhiều hàm xử lý mỗi sự kiện.
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
- C không có lambda. Cái khó không phải hàm không tên, mà là bao đóng: hàm cộng môi trường nó bắt được.
- Cách chuẩn và duy nhất đáng dùng là cặp con trỏ hàm với con trỏ ngữ cảnh, gói trong một struct.
- Bạn tự quyết ngữ cảnh nằm ở đâu, sống bao lâu, và ai giải phóng. Đó vừa là gánh nặng vừa là sự linh hoạt.
- Compound literal của C99 cho phép tạo ngữ cảnh ngay tại chỗ, nhưng nó chỉ sống tới hết khối lệnh.
- Hàm lồng của GCC sinh mã trên ngăn xếp và vô hiệu hóa bảo vệ NX của cả chương trình. Biết để đọc mã cũ, không dùng.