Bộ tiền xử lý – Preprocessor trong C/C++ | CppDeveloper

Preprocessor ( bộ tiền giải quyết và xử lý ) là một khái niệm đặc trưng trong C / C + +, đó là một công cụ được thực thi trước khi quy trình biên dịch thực sự được triển khai .

Bộ tiền xử lý có nhiệm vụ xử lý các chỉ thị tiền xử lý, như #include, #define, #if, #ifdef, #ifndef, #endif,… Nó làm việc trên một file source C++ tại một thời điểm bằng cách thay thế các chỉ thị #include bằng nội dung của các file tương ứng (thường chỉ chứa khai báo), thay thế các macro #define và chọn các phần khác nhau trong source code để biên dịch tùy thuộc vào các chỉ thị #if, #ifdef, #ifndef.

Trong bài này mình sẽ tổng hợp 1 số  best practice về Preprocessor

Include Guards

Trong bài này mình sẽ tổng hợp 1 số best practice về Preprocessor

Một file source có thể include một hoặc nhiều file header. Và một file header có thể include một hoặc nhiều file header khác. Do đó một source file include nhiều file header thì có thể gián tiếp include một file header nào đó nhiều lần. Nếu một file header được include nhiều lần mà trong file đó có chứa định nghĩa struct, class, … thì khi biên dịch sẽ bị lỗi.

“include guards” thường được dùng để tránh lỗi này. Để áp dụng “include guards” thì chúng ta sẽ sử dụng các chỉ thị #define, #ifndef, #endif. Ví dụ →

1234567

/ / Foo. h

# ifndef FOO_H

# define FOO_H

classFoo{/ / a class definition

};

# endif / / FOO_H

“ include guards ” hoàn toàn có thể work ok với toàn bộ những standard compiler và preprocessor. Vấn đề mà những dev cần chú ý quan tâm ở đây là làm thế nào bảo vệ tính duy nhất của macro dùng để guard. Ví dụ trong trường hợp này, nếu có nhiều hơn 1 header file cùng sử dụng FOO_H làm macro để guard thì hoàn toàn có thể dẫn đến lỗi do include thiếu header. Và nếu trong project của tất cả chúng ta có sử dụng thư viện của bên thứ 3 nữa thì năng lực bị trùng sẽ càng cao .
Ngoài ra với cách này cũng cần phải bảo vệ macro sử dụng trong “ include guards ” cũng phải khác với tổng thể những macro được define trong tổng thể header của khác .

Để tránh rắc rối khi sử dụng “include guards” bằng macro thì hầu hết các trình biên dịch C++ hiện nay đều support chỉ thị #pragma_once để đảm bảo một header chỉ được include một lần vào 1 file source. Ví dụ →

12345

/ / Foo. h

# pragma once

classFoo{

};

Tuy nhiên ae phải nhớ rằng cái này ko thuộc ISO C + + standard nên không bảo vệ tổng thể những trình biên dịch đều tương hỗ nhé, và những compiler ko tương hỗ sẽ bí mật lặng lẽ bỏ lỡ thông tư này .
Bản thân mình thì vẫn quen dùng “ include guards ” bằng macro mặc dầu nhiều lúc cũng bị trùng với macro trong những 3 rd libraries .

Conditional pre-processing logic

“ Conditional pre-processing logic ” – “ tiền giải quyết và xử lý điều kiện kèm theo logic ” là việc làm cho những phần code nào đó trở nên available hoặc unavailable trong quy trình biên dịch bằng cách sử dụng những thông tư tiền giải quyết và xử lý điều kiện kèm theo .
Một số use-cases điển hình →

  • Cùng một app nhưng có các build mode khác nhau (debug, release, testing), mỗi mode lại có các phần code log thêm khác nhau hoặc log level cũng khác nhau
  • Cùng một source code nhưng sử dụng để build binary để run trên các platform khác nhau
  • Cùng một source code nhưng sử dụng để build binary cho các variant khác nhau của phần mềm. Ví dụ : dùng chung source code cho các bản Basic, Plus, và Premium của cùng 1 phần mềm chỉ khác nhau một chút về tính năng.

* Ví dụ 1: Sử dụng Conditional pre-processing trong cross-plaform source code (source code hỗ trợ nhiều platform)

12345678910

uint32_tThreadUtil::getThreadId(){

# if ( defined ( _WIN32 ) | | defined ( _WIN64 ) )

std::stringstreamss;

ss<

uint64_tid=std::stoull(ss.str());

return(uint32_t)id;

# elif __unix__

returnpthread_self();

# endif

}

Các macro như _WIN32, _WIN64 hay __unix__ được compiler tự động define dựa vào cấu hình build

* Ví dụ 2: Mở code in log riêng với chế độ build Testing

1234567891011

voids_PrintAppStateOnUserPrompt()

{

std::cout<<" -------- BEGIN-DUMP --------------- \ n "

<Settings().ToString()<<" \ n "

# if ( 1 = = TESTING_MODE ) / / privacy : we want user details only when testing

<GetActionNames())

<CrntDocument().Name()

<CrntDocument().SignatureSHA()<<" \ n "

# endif

<<" -------- END-DUMP --------------- \ n "

}

* Ví dụ 3: Implement 1 tính năng mà chỉ có ở bản Premium

1234567

# ifdef _PREMIUM

voidstartAutoParking

{

/ / code logic here

}

# endif

Lưu ý:
Nếu một macro không được define và giá trị của nó được dùng để so sánh và kiểm tra bởi preprocessor thì preprocessor sẽ mặc định giá trị của macro đó bằng 0 .

Macro

Macro là hoàn toàn có thể coi một kỹ thuật dùng để copy paste code tự động hóa tại thời gian biên dịch, ở đó những đoạn code được lặp lại ở nhiều chỗ sẽ được đóng thành macro và sẽ được preprocessor đưa vào nơi thiết yếu trước khi biên dịch. macro hoàn toàn có thể chia thành 2 loại chính : object-like và function-like .
Ví dụ →

123456789

/ / This is an object-like macro

# define PI 3.14159265358979

/ / This is a function-like macro .

# define AREA ( r ) ( PI * ( r ) * ( r ) )

/ / They can be used like this :

doublepi_macro=PI;

doublearea_macro=AREA(4.6);

123456

doublepi_squared=PI*PI;

/ / Compiler sees :

doublepi_squared=3.14159265358979*

3.14159265358979;

doublearea=AREA(5);

/ / Compiler sees :

doublearea=(3.14159265358979*(5)*(5))

Ví dụ → Macro thường được viết hoa hàng loạt để dễ phân biệt khi đọc code. * Khi preprocessor gặp một object-like macro thì hành vi của nó đơn thuần chỉ là copy-paste, macro name sẽ được thay thế sửa chữa bởi định nghĩa của nó. Còn khi gặp một function-like macro, macro name sẽ được sửa chữa thay thế bởi định nghĩa của nó đồng thời những tham số cũng được thay bởi tên tham số thật. Ví dụ →* Các ae code C / C + + luôn luôn có thói quen kết thúc 1 dòng lệnh bằng dấu ; và đôi lúc điều đó lại gây lỗi rất vớ vẩn khi dùng macro. Ví dụ →

12345

# define IF_BREAKER ( Func ) Func ( ) ;

if(…)

IF_BREAKER(some_func);

else

…;

1

error:’ else ‘withoutaprevious’ if ‘

123456

# define DELETE_PTR ( ptr ) \

if(nullptr!=ptr)\

{\

deleteptr;\

ptr=nullptr;\

}

__VA_ARGS__ trong phần định nghĩa macro. Ví dụ →

12

# define VARIADIC ( Param, … ) Param ( __VA_ARGS__ )

VARIADIC(printf,” % d “,8);

1

printf(” % d “,8);

Predefined macros

Trong ví dụ này việc có 2 dấu ; ở cuối line 3 sẽ làm cho compiler không nhận ra else ở line số 4 là cùng block với lệnh if ở line số 2 dẫn đến compiler lỗiDo đó ae cần rất là chú ý khi dùng dấu ; trong định nghĩa macro * Trong trường hợp định nghĩa macro quá dài thì hoặc muốn tách ra nhiều line cho dễ nhìn thì hoàn toàn có thể sử dụng ký tự backslash ở cuối dòng để nối xuống dòng tiếp theo → * Variadic macros : Macro này đặc biệt quan trọng ở chỗ hoàn toàn có thể lấy một số lượng tham số ( không xác lập trước ) thay vàotrong phần định nghĩa macro. Ví dụ → sau khi preprocessor giải quyết và xử lý thì compiler sẽ thấy như thế nàyPredefined macros là những macro được define bởi trình biên dịch. Ae chớ có nghịch ngu mà define lại ( re-define ) hoặc undefine những predefined macro nhé .
Theo C + + standard thì những macro sau được sẽ được predefined bởi compiler →

  • __LINE__ : line number của dòng code chứa macro này
  • __FILE__: tên của file chứa macro này
  • __DATE__: ngày mà file code chứa macro này được biên dịch, có định dạng “Mmm dd yyyy”
  • __TIME__: timemà file code chứa macro này được biên dịch, có định dạng “hh:mm:ss”
  • __cplusplus: được define bởi C++ compiler khi đang biên dịch file c++

Ngoài ra còn 1 số predefined macro khác nữa nhưng ko thông dụng và ít gặp trong code ứng dụng thường thì nên mình ko đề cập vào đây .
Ngoài những standard predefined macro thì những compiler khác nhau cũng có những bộ predefined macro riêng của chúng nữa. Những cái này phải đọc documents của compiler mới biết được. Biết vậy thôi chứ thông thường đọc làm gì cho đau đầu, khi nào cần thì research thôi .
Một số ví dụ sử dụng predefined macro →

12345

# ifdef __cplusplus / / if compiled by C + + compiler

extern” C “{/ / C code has to be decorated

/ / C library header declarations here

}

# endif

1234567

boolsuccess=doSomething(/ * some arguments * /);

if(!success){

std::cerr<<" ERROR : doSomething ( ) failed on line "<<__LINE__-2

<<" in function "<<__func__<<" ( ) "

<<" in file "<<__FILE__

<

}

Preprocessor Operators (Toán tử tiền xử lý)

Một số ví dụ sử dụng predefined macro →

* Toán tử # hay còn gọi là “stringizing operator” được sử dụng để chuyển 1 tham số của macro thành string. chỉ có thể sử dụng với macro có tham số. Ví dụ →

12

# define PRINT ( x ) printf ( # x ” \ n ” )

PRINT(Thislinewillbeconvertedtostringbypreprocessor);

1

printf(” This line will be converted to string by preprocessor “” \ n “);

## hay còn gọi là “Token pasting operator” được sử dụng để nối 2 tham số của macro. Ví dụ →

123

# define PRINT ( x ) printf ( ” variable ” # x ” = % d “, variable # # x )

intvariableY=15;

PRINT(Y);

1

printf(” variable “” Y “” = % d “,variableY);

1

variableY=15

Preprocessor error messages

↓ sẽ được preprocessor convert thành * Toán tửhay còn gọi là “ Token pasting operator ” được sử dụng để nối 2 tham số của macro. Ví dụ → ↓ sẽ được preprocessor convert thànhoutput sẽ làLỗi compile hoàn toàn có thể được định nghĩa sử dụng preprocessor. Nó khá hữu dụng trong trường hợp cần thông tin lỗi compile tương quan đến platform hoặc compiler version .
Ví dụ : khi compile source code có đoạn code sau thì sẽ gặp lỗi báo lỗi compile “ This code requires gcc > 3.0.0 ” nếu gcc version < = 3.0.0

123

# if __GNUC__ < 3

# error ” This code requires gcc > 3.0.0 “

# endif

123

# ifdef __APPLE__

# error ” Apple products are not supported in this release “

# endif

* Ví dụ : khi compile source code có đoạn code sau thì sẽ gặp lỗi báo lỗi compile “ Apple products are not supported in this release ” nếu compile code để chạy trên Apple device— Phạm Minh Tuấn ( Shun ) —