Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 30.524 phút đọc

_Generic của C11

Sau bài này bạn sẽ làm được

  • Viết macro chọn hàm theo kiểu đối số bằng _Generic
  • Nêu bốn giới hạn của _Generic so với nạp chồng thật
  • Kết hợp _Generic với macro để làm giao diện tổng quát
  • Biết tgmath.h được xây trên cơ chế này

C11 thêm một cách chọn biểu thức theo kiểu của đối số, ngay lúc biên dịch. Nó là thứ gần nhất với nạp chồng hàm mà C từng có, và nó là nền của tgmath.h. Nó cũng có bốn giới hạn khiến nó không phải nạp chồng thật.

#Cú pháp và cách nó chạy

_Generic(bieu_thuc_dieu_khien,
         kieu1:   ket_qua1,
         kieu2:   ket_qua2,
         default: ket_qua_mac_dinh)
_Generic
Biểu thức chọn một trong các nhánh dựa trên kiểu của biểu thức điều khiển. Việc chọn diễn ra lúc biên dịch, và các nhánh không được chọn thậm chí không cần hợp lệ về mặt ngữ nghĩa.
gen.c
#include <math.h>
#include <stdlib.h>

#define ABS(x) _Generic((x),        \
        int:         abs,           \
        long:        labs,          \
        long long:   llabs,         \
        float:       fabsf,         \
        double:      fabs,          \
        long double: fabsl,         \
        default:     abs)(x)

#define TEN_KIEU(x) _Generic((x),                     \
        int:          "int",                          \
        unsigned:     "unsigned",                     \
        double:       "double",                       \
        float:        "float",                        \
        char:         "char",                         \
        char *:       "char *",                       \
        const char *: "const char *",                 \
        default:      "khong biet")
terminal
gcc -std=c11 -Wall -Wextra -o gen gen.c && ./gen
ABS(-5)      = 5
ABS(-5.5)    = 5.5
ABS(-5.5f)   = 5.5

TEN_KIEU(3)      = int
TEN_KIEU(3.14)   = double
TEN_KIEU(3.14f)  = float
TEN_KIEU('a')    = int
TEN_KIEU(c)      = char
TEN_KIEU("abc")  = char *
TEN_KIEU(m)      = char *
TEN_KIEU(q)      = const char *
TEN_KIEU(i + d)  = double

#Bốn quy tắc về kiểu

Một: mảng suy biến thành con trỏ

char m[10];

TEN_KIEU(m)      /* -> "char *", KHONG phai "char[10]" */

/* Bieu thuc dieu khien duoc chuyen doi lvalue, tuc la:
     - mang suy bien thanh con tro
     - ham suy bien thanh con tro ham
     - bo qualifier cua kieu mang

   Nen ban KHONG viet duoc nhanh cho "char[10]". */

Hai: hằng ký tự có kiểu int

TEN_KIEU('a')    /* -> "int" */

char c = 'a';
TEN_KIEU(c)      /* -> "char" */

/* Trong C, hang ky tu 'a' co kieu int, khac han C++ noi no la char.
   Bai 3.3 da nhac dieu nay. Neu macro cua ban co nhanh cho char
   ma nguoi dung truyen mot hang ky tu thi ho se roi vao nhanh int. */

Ba: char, signed char, unsigned char là ba kiểu khác nhau

_Generic((x),
    char:          "char",
    signed char:   "signed char",
    unsigned char: "unsigned char")

/* Ba nhanh nay hop le va khong trung nhau, du tren mot he thong
   cu the thi char giong het mot trong hai cai kia ve bieu dien.

   Cung the voi:
     int va long   tren he 32 bit, dau la 4 byte nhung la hai kieu
     long va long long

   Neu ban chi viet nhanh cho int ma nguoi dung truyen long,
   ho roi vao default hoac bi loi bien dich. */

Bốn: const và volatile không bị bỏ

const char *q;

TEN_KIEU(q)      /* -> "const char *", KHONG roi vao nhanh "char *" */

/* Qualifier o TANG NGOAI CUNG cua bieu thuc dieu khien thi bi bo:
     const int a;  TEN_KIEU(a) -> "int"

   Nhung qualifier ben trong kieu con tro thi KHONG:
     const char *p;  -> kieu la "con tro toi const char", giu nguyen

   Nen mot macro nhan chuoi phai co CA HAI nhanh: */

#define DO_DAI(x) _Generic((x),           \
        char *:       strlen,             \
        const char *: strlen)(x)

#Bốn giới hạn

Giới hạnNạp chồng thật làm được_Generic
Chọn theo SỐ đối sốCóKhông, phải thêm macro đếm đối số
Chọn theo kiểu TRẢ VỀCó, trong vài ngôn ngữKhông, chỉ nhìn đối số
Chuyển đổi ngầm tới kiểu gần nhấtCóKhông, phải khớp kiểu CHÍNH XÁC
Áp dụng cho kiểu do người dùng thêm sauCóKhông, danh sách nhánh cố định lúc viết macro

#Ba ứng dụng thật

Một: in giá trị mà không cần nhớ đặc tả

#include <inttypes.h>
#include <stdio.h>

#define DAC_TA(x) _Generic((x),                 \
        char:               "%c",               \
        signed char:        "%hhd",             \
        unsigned char:      "%hhu",             \
        short:              "%hd",              \
        unsigned short:     "%hu",              \
        int:                "%d",               \
        unsigned:           "%u",               \
        long:               "%ld",              \
        unsigned long:      "%lu",              \
        long long:          "%lld",             \
        unsigned long long: "%llu",             \
        float:              "%g",               \
        double:             "%g",               \
        long double:        "%Lg",              \
        char *:             "%s",               \
        const char *:       "%s",               \
        void *:             "%p",               \
        default:            "%p")

#define IN(x)      printf(DAC_TA(x), (x))
#define IN_DONG(x) (printf(#x " = "), IN(x), putchar('\n'))

int    a = 42;
double d = 3.14;
size_t n = 100;

IN_DONG(a);        /* a = 42     */
IN_DONG(d);        /* d = 3.14   */
IN_DONG(n);        /* n = 100    */
IN_DONG(a * 2);    /* a * 2 = 84 */

Hai: giao diện tổng quát cho container

/* Bai 30.2 sinh ra VecInt_day, VecDouble_day, ...
   Nguoi dung phai nho ten dai. _Generic gom lai mot ten: */

#define vec_day(v, x) _Generic((v),         \
        VecInt *:    VecInt_day,            \
        VecDouble *: VecDouble_day,         \
        VecChar *:   VecChar_day)(v, x)

VecInt    a;
VecDouble b;

vec_day(&a, 42);        /* -> VecInt_day(&a, 42)     */
vec_day(&b, 3.14);      /* -> VecDouble_day(&b, 3.14) */

/* Gioi han: danh sach nhanh phai liet ke MOI kieu vector
   ma du an dung, va no nam trong mot header. Nguoi dung
   thu vien khong them kieu cua ho vao duoc.

   Do la ly do thu vien nhu klib khong dung _Generic ma bat
   nguoi dung goi ten day du. */

Ba: tgmath.h của thư viện chuẩn

#include <tgmath.h>

float       f = 2.0f;
double      d = 2.0;
long double l = 2.0L;

sqrt(f);      /* goi sqrtf */
sqrt(d);      /* goi sqrt  */
sqrt(l);      /* goi sqrtl */

/* Truoc C99 ban phai tu nho ba ten. tgmath.h dinh nghia
   sqrt la mot macro HAM chon mot trong ba: */

#define sqrt(z) __TGMATH_CPLX(z, sqrt, csqrt)

/* Vi no la macro HAM, no chi bung khi co dau ngoac ngay sau ten.
   Nen dong nay VAN chay: */

double (*p)(double) = sqrt;    /* sqrt khong theo sau boi (
                                  -> macro khong bung
                                  -> day la ham sqrt that */

printf("%f\n", p(2.0));       /* 1.414214 */

Tự làm thử

  1. Viết macro TEN_KIEU và thử với hằng ký tự, biến char, chuỗi hằng, mảng char, và const char *.
  2. Viết ABS bằng _Generic và kiểm với int, float, double.
  3. Viết hai nhánh cùng kiểu qua hai typedef và đọc thông báo lỗi.
  4. Viết macro IN_DONG và dùng nó với size_t, xác nhận nó in đúng mà không cần bạn nhớ %zu.
  5. Chạy TEN_KIEU(i++) rồi in i, xác nhận nó không đổi.
  6. Thử lấy địa chỉ của sqrt sau khi include tgmath.h và sau khi include math.h, so hai kết quả.

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

  • _Generic chọn một nhánh theo kiểu của biểu thức điều khiển, ngay lúc biên dịch, và không tốn gì lúc chạy.
  • Mảng suy biến thành con trỏ, hằng ký tự có kiểu int, và const char * không rơi vào nhánh char *.
  • Nó khớp kiểu chính xác, không xét chuyển đổi ngầm, nên một macro đầy đủ cho số nguyên cần mười một nhánh.
  • Mọi nhánh phải hợp lệ về cú pháp, nên hãy để mỗi nhánh chỉ là một tên hàm.
  • Biểu thức điều khiển không được đánh giá, chỉ kiểu của nó được dùng.