Tại sao macro bộ xử lý trước lại là ác và các lựa chọn thay thế là gì?


94

Tôi đã luôn hỏi điều này nhưng tôi chưa bao giờ nhận được một câu trả lời thực sự tốt; Tôi nghĩ rằng hầu như bất kỳ lập trình viên nào trước khi viết "Hello World" đầu tiên đã gặp phải một cụm từ như "không bao giờ nên sử dụng macro", "macro là ác", v.v., câu hỏi của tôi là: tại sao? Với C ++ 11 mới, liệu có một giải pháp thay thế thực sự sau nhiều năm?

Phần dễ dàng là về các macro như #pragma, dành riêng cho nền tảng và trình biên dịch cụ thể, và hầu hết thời gian chúng có những sai sót nghiêm trọng như #pragma oncevậy dễ xảy ra lỗi trong ít nhất 2 tình huống quan trọng: trùng tên trong các đường dẫn khác nhau và với một số thiết lập mạng và hệ thống tệp.

Nhưng nói chung, những gì về macro và các lựa chọn thay thế cho việc sử dụng chúng?


19
#pragmakhông phải là vĩ mô.
FooF

1
@foof chỉ thị tiền xử lý?
user1849534

6
@ user1849534: Vâng, đó là những gì ... và lời khuyên liên quan đến macro không được nói đến #pragma.
Ben Voigt

1
Bạn có thể làm được nhiều điều với constexpr, các inlinehàm và templates, nhưng boost.preprocessorchaoscho thấy rằng macro có vị trí của chúng. Chưa kể macro cấu hình cho các trình biên dịch, nền tảng khác nhau, v.v.
Brandon

1
Có thể trùng lặp các macro C ++ có lợi khi nào?
Aaron McDaid

Câu trả lời:


165

Macro cũng giống như bất kỳ công cụ nào khác - một cái búa được sử dụng trong một vụ giết người không phải là xấu xa vì nó là một cái búa. Nó xấu xa theo cách mà người sử dụng nó theo cách đó. Nếu bạn muốn đóng đinh, búa là một công cụ hoàn hảo.

Có một số khía cạnh đối với macro khiến chúng trở nên "xấu" (tôi sẽ mở rộng về từng khía cạnh sau và đề xuất các lựa chọn thay thế):

  1. Bạn không thể gỡ lỗi macro.
  2. Mở rộng vĩ mô có thể dẫn đến các tác dụng phụ kỳ lạ.
  3. Macro không có "không gian tên", vì vậy nếu bạn có macro xung đột với tên được sử dụng ở nơi khác, bạn sẽ nhận được các thay thế macro ở nơi bạn không muốn và điều này thường dẫn đến các thông báo lỗi lạ.
  4. Macro có thể ảnh hưởng đến những điều bạn không nhận ra.

Vì vậy, hãy mở rộng một chút ở đây:

1) Không thể gỡ lỗi macro. Khi bạn có macro dịch thành một số hoặc một chuỗi, mã nguồn sẽ có tên macro và nhiều trình gỡ lỗi, bạn không thể "nhìn thấy" macro dịch sang. Vì vậy, bạn thực sự không biết những gì đang xảy ra.

Thay thế : Sử dụng enumhoặcconst T

Đối với macro "giống như hàm", vì trình gỡ lỗi hoạt động ở cấp độ "mỗi dòng nguồn nơi bạn ở", macro của bạn sẽ hoạt động giống như một câu lệnh duy nhất, bất kể đó là một câu lệnh hay một trăm câu lệnh. Làm cho khó để tìm ra những gì đang xảy ra.

Thay thế : Sử dụng các hàm - nội tuyến nếu nó cần "nhanh" (nhưng lưu ý rằng quá nhiều nội tuyến không phải là điều tốt)

2) Mở rộng vĩ mô có thể có tác dụng phụ lạ.

Một trong những nổi tiếng là #define SQUARE(x) ((x) * (x))và sử dụng x2 = SQUARE(x++). Điều đó dẫn đến x2 = (x++) * (x++);, ngay cả khi nó là mã hợp lệ [1], gần như chắc chắn sẽ không phải là điều mà lập trình viên mong muốn. Nếu đó là một hàm, bạn có thể thực hiện x ++, và x sẽ chỉ tăng một lần.

Một ví dụ khác là "if else" trong macro, giả sử chúng ta có điều này:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

và sau đó

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

Nó thực sự trở thành một điều hoàn toàn sai lầm ....

Thay thế : các chức năng thực.

3) Macro không có không gian tên

Nếu chúng ta có một macro:

#define begin() x = 0

và chúng tôi có một số mã trong C ++ sử dụng begin:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

Bây giờ, bạn nghĩ bạn nhận được thông báo lỗi nào và bạn tìm lỗi ở đâu [giả sử bạn đã hoàn toàn quên - hoặc thậm chí không biết về - macro bắt đầu nằm trong một số tệp tiêu đề mà người khác đã viết? [và thậm chí còn thú vị hơn nếu bạn bao gồm macro đó trước khi bao gồm - bạn sẽ bị chìm trong những lỗi kỳ lạ hoàn toàn không có ý nghĩa khi bạn nhìn vào chính mã.

Thay thế : Không có quá nhiều thay thế như một "quy tắc" - chỉ sử dụng tên viết hoa cho macro và không bao giờ sử dụng tất cả tên hoa cho những thứ khác.

4) Macro có những hiệu ứng mà bạn không nhận ra

Thực hiện chức năng này:

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ... 
void dostuff()
{
    int x = 7;

    begin();

    ... more code using x ... 

    printf("x=%d\n", x);

    end();

}

Bây giờ, nếu không nhìn vào macro, bạn sẽ nghĩ rằng begin là một hàm, không nên ảnh hưởng đến x.

Điều này và tôi đã thấy nhiều ví dụ phức tạp hơn, THỰC SỰ có thể làm rối tung cả ngày của bạn!

Thay thế : Không sử dụng macro để đặt x hoặc chuyển x vào làm đối số.

Có những lúc việc sử dụng macro chắc chắn có lợi. Một ví dụ là bọc một hàm bằng macro để chuyển thông tin về tệp / dòng:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Bây giờ chúng ta có thể sử dụng my_debug_mallocnhư malloc thông thường trong mã, nhưng nó có thêm các đối số, vì vậy khi nói đến phần cuối và chúng ta quét "phần tử bộ nhớ nào chưa được giải phóng", chúng ta có thể in nơi phân bổ được thực hiện để lập trình viên có thể theo dõi sự rò rỉ.

[1] Cập nhật một biến nhiều lần "trong một điểm trình tự" là hành vi không xác định. Một điểm trình tự không hoàn toàn giống với một câu lệnh, nhưng đối với hầu hết các ý định và mục đích, đó là những gì chúng ta nên coi nó là. Vì vậy, thực hiện x++ * x++sẽ cập nhật xhai lần, không được xác định và có thể sẽ dẫn đến các giá trị khác nhau trên các hệ thống khác nhau và giá trị kết quả cũng khác nhau x.


6
Các if elsevấn đề có thể được giải quyết bằng cách bọc phần thân macro bên trong do { ... } while(0). Điều này hoạt động như người ta mong đợi đối với ifforvà các vấn đề về luồng kiểm soát tiềm ẩn rủi ro khác. Nhưng có, một chức năng thực thường là một giải pháp tốt hơn. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Aaron McDaid

11
@AaronMcDaid: Có, có một số cách giải quyết để giải quyết một số vấn đề trong các macro này. Toàn bộ điểm của bài viết của tôi không phải là chỉ ra cách làm tốt macro, mà là "làm thế nào dễ dàng mắc sai lầm macro", nơi có một giải pháp thay thế tốt. Điều đó nói lên rằng, có những thứ mà macro giải quyết rất dễ dàng, và có những lúc macro cũng là điều đúng đắn để làm.
Mats Petersson

1
Ở điểm 3, các lỗi thực sự không còn là vấn đề nữa. Các trình biên dịch hiện đại như Clang sẽ nói một cái gì đó giống như note: expanded from macro 'begin'và hiển thị nơi beginđược xác định.
kirbyfan64sos

5
Macro khó dịch sang các ngôn ngữ khác.
Marco van de Voort

1
@FrancescoDondi: stackoverflow.com/questions/4176328/... . (Khá một chút xuống trong câu trả lời đó, nó nói về i ++ * i ++ và như vậy
Mats Petersson

21

Câu nói "macro là ác" thường ám chỉ việc sử dụng #define, không phải #pragma.

Cụ thể, biểu thức đề cập đến hai trường hợp sau:

  • xác định số ma thuật dưới dạng macro

  • sử dụng macro để thay thế các biểu thức

với C ++ 11 mới có một giải pháp thay thế thực sự sau nhiều năm?

Có, đối với các mục trong danh sách trên (các số ma thuật phải được định nghĩa bằng const / constexpr và các biểu thức phải được định nghĩa bằng các hàm [normal / inline / template / inline template].

Dưới đây là một số vấn đề được đưa ra bằng cách xác định số ma thuật dưới dạng macro và thay thế biểu thức bằng macro (thay vì xác định các hàm để đánh giá các biểu thức đó):

  • khi xác định macro cho số ma thuật, trình biên dịch không giữ lại thông tin kiểu cho các giá trị đã xác định. Điều này có thể gây ra cảnh báo biên dịch (và lỗi) và gây nhầm lẫn cho người gỡ lỗi mã.

  • khi xác định macro thay vì hàm, các lập trình viên sử dụng mã đó mong đợi chúng hoạt động giống như các hàm và họ không làm như vậy.

Hãy xem xét mã này:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

Bạn sẽ mong đợi a và c là 6 sau khi gán cho c (như vậy, với việc sử dụng std :: max thay vì macro). Thay vào đó, mã thực hiện:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

Trên hết, macro không hỗ trợ không gian tên, có nghĩa là việc xác định macro trong mã của bạn sẽ giới hạn mã máy khách trong những tên mà chúng có thể sử dụng.

Điều này có nghĩa là nếu bạn xác định macro ở trên (cho tối đa), bạn sẽ không thể thực hiện #include <algorithm>bất kỳ mã nào bên dưới nữa, trừ khi bạn viết rõ ràng:

#ifdef max
#undef max
#endif
#include <algorithm>

Có macro thay vì biến / hàm cũng có nghĩa là bạn không thể lấy địa chỉ của chúng:

  • nếu một macro-as-hằng số đánh giá một số ma thuật, bạn không thể chuyển nó theo địa chỉ

  • đối với macro-as-function, bạn không thể sử dụng nó như một vị từ hoặc lấy địa chỉ của hàm hoặc coi nó như một hàm chức năng.

Chỉnh sửa: Như một ví dụ, thay thế chính xác cho những điều #define maxtrên:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

Điều này thực hiện mọi thứ mà macro thực hiện, với một hạn chế: nếu các loại đối số khác nhau, phiên bản mẫu buộc bạn phải rõ ràng (điều này thực sự dẫn đến mã an toàn hơn, rõ ràng hơn):

int a = 0;
double b = 1.;
max(a, b);

Nếu giá trị tối đa này được xác định là macro, mã sẽ biên dịch (kèm theo cảnh báo).

Nếu giá trị tối đa này được định nghĩa là một hàm mẫu, trình biên dịch sẽ chỉ ra sự không rõ ràng và bạn phải nói hoặc max<int>(a, b)hoặc max<double>(a, b)(và do đó nêu rõ ý định của bạn).


1
Nó không nhất thiết phải là c ++ 11 cụ thể; bạn có thể chỉ cần sử dụng các hàm để thay thế việc sử dụng macro dưới dạng biểu thức và [static] const / constexpr để thay thế việc sử dụng macro-as-hằng số.
utnapistim

1
Ngay cả C99 cũng cho phép sử dụng const int someconstant = 437;, và nó có thể được sử dụng hầu hết mọi cách mà macro sẽ được sử dụng. Tương tự như vậy đối với các chức năng nhỏ. Có một số thứ mà bạn có thể viết thứ gì đó dưới dạng macro sẽ không hoạt động trong biểu thức chính quy trong C (bạn có thể tạo thứ gì đó tính trung bình một mảng của bất kỳ loại số nào, điều mà C không thể làm - nhưng C ++ có các mẫu cho rằng). Trong khi C ++ 11 bổ sung thêm một số thứ mà "bạn không cần macro cho việc này", nó hầu như đã được giải quyết trong C / C ++ trước đó.
Mats Petersson

Thực hiện tăng trước trong khi chuyển đối số là một thực hành mã hóa tồi tệ. Và bất kỳ ai viết mã bằng C / C ++ không nên cho rằng một lệnh gọi giống như hàm không phải là macro.
StephenG

Nhiều triển khai tự nguyện tạo dấu ngoặc đơn maxminnếu chúng được theo sau bởi dấu ngoặc đơn bên trái. Nhưng bạn không nên xác định các macro như vậy ...
LF

14

Một vấn đề phổ biến là:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Nó sẽ in 10, không phải 5, vì bộ xử lý trước sẽ mở rộng nó theo cách này:

printf("25 / (3+2) = %d", 25 / 3 + 2);

Phiên bản này an toàn hơn:

#define DIV(a,b) (a) / (b)

2
ví dụ thú vị, về cơ bản họ chỉ mã thông báo mà không cần ngữ nghĩa
user1849534

Đúng. Chúng được mở rộng theo cách chúng được cung cấp cho macro. Các DIVvĩ mô có thể được viết lại với một cặp () xung quanh b.
phaazon

2
Ý bạn là #define DIV(a,b), không phải #define DIV (a,b), rất khác.
rici

6
#define DIV(a,b) (a) / (b)không đủ tốt; như một vấn đề của thực tế nói chung, luôn luôn thêm ngoài cùng dấu ngoặc, như thế này:#define DIV(a,b) ( (a) / (b) )
PJTraill

3

Macro có giá trị đặc biệt để tạo mã chung (tham số của macro có thể là bất kỳ thứ gì), đôi khi có tham số.

Hơn nữa, mã này được đặt (tức là được chèn) tại điểm macro được sử dụng.

OTOH, các kết quả tương tự có thể đạt được với:

  • hàm quá tải (các loại tham số khác nhau)

  • mẫu, trong C ++ (kiểu và giá trị tham số chung)

  • các hàm nội tuyến (đặt mã nơi chúng được gọi, thay vì nhảy đến định nghĩa một điểm - tuy nhiên, đây đúng hơn là một lệnh gợi lại cho trình biên dịch).

chỉnh sửa: vì lý do tại sao macro bị lỗi:

1) không kiểm tra kiểu của các đối số (chúng không có kiểu), vì vậy có thể dễ dàng bị sử dụng sai 2) đôi khi mở rộng thành mã rất phức tạp, có thể khó xác định và hiểu trong tệp được xử lý trước 3) dễ gây ra lỗi -prone code trong macro, chẳng hạn như:

#define MULTIPLY(a,b) a*b

và sau đó gọi

MULTIPLY(2+3,4+5)

mở rộng trong

2 + 3 * 4 + 5 (và không thành: (2 + 3) * (4 + 5)).

Để có cái sau, bạn nên xác định:

#define MULTIPLY(a,b) ((a)*(b))

3

Tôi không nghĩ rằng có gì sai khi sử dụng các định nghĩa hoặc macro tiền xử lý như bạn gọi chúng.

Chúng là một khái niệm ngôn ngữ (meta) được tìm thấy trong c / c ++ và giống như bất kỳ công cụ nào khác, chúng có thể giúp cuộc sống của bạn dễ dàng hơn nếu bạn biết mình đang làm gì. Rắc rối với macro là chúng được xử lý trước mã c / c ++ của bạn và tạo ra mã mới có thể bị lỗi và gây ra lỗi trình biên dịch là điều hiển nhiên. Về mặt sáng sủa, chúng có thể giúp bạn giữ cho mã của mình sạch sẽ và giúp bạn tiết kiệm rất nhiều lần nhập nếu được sử dụng đúng cách, vì vậy nó phụ thuộc vào sở thích cá nhân.


Ngoài ra, như được chỉ ra bởi các câu trả lời khác, các định nghĩa tiền xử lý được thiết kế kém có thể tạo ra mã có cú pháp hợp lệ nhưng ý nghĩa ngữ nghĩa khác nhau, điều đó có nghĩa là trình biên dịch sẽ không phàn nàn và bạn đã đưa ra một lỗi trong mã của mình, điều này thậm chí còn khó tìm hơn.
Sandi Hrvić

3

Macro trong C / C ++ có thể đóng vai trò như một công cụ quan trọng để kiểm soát phiên bản. Có thể gửi cùng một mã cho hai máy khách với một cấu hình nhỏ của Macro. Tôi sử dụng những thứ như

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

Loại chức năng này không thể dễ dàng như vậy nếu không có macro. Macro thực sự là một Công cụ quản lý cấu hình phần mềm tuyệt vời và không chỉ là một cách để tạo lối tắt để sử dụng lại mã. Việc xác định các chức năng với mục đích tái sử dụng trong macro chắc chắn có thể tạo ra vấn đề.


Đặt giá trị Macro trên cmdline trong quá trình biên dịch để tạo hai biến thể từ một cơ sở mã thực sự rất hay. trong chừng mực.
kevinf

1
Từ góc độ nào đó, cách sử dụng này là nguy hiểm nhất: các công cụ (IDE, máy phân tích tĩnh, tái cấu trúc) sẽ gặp khó khăn trong việc tìm ra các đường dẫn mã có thể có.
ernon

1

Tôi nghĩ rằng vấn đề là các macro không được trình biên dịch tối ưu hóa tốt và rất "xấu xí" để đọc và gỡ lỗi.

Thường thì các lựa chọn thay thế tốt là các hàm chung chung và / hoặc các hàm nội tuyến.


2
Điều gì khiến bạn tin rằng macro không được tối ưu hóa tốt? Chúng là sự thay thế văn bản đơn giản và kết quả được tối ưu hóa giống như mã được viết mà không có macro.
Ben Voigt

@BenVoigt nhưng họ không xem xét ngữ nghĩa và điều này có thể dẫn đến điều gì đó có thể được coi là "không tối ưu" ... ít nhất đây là suy nghĩ đầu tiên của tôi về stackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534: Đó không phải là ý nghĩa của từ "tối ưu hóa" trong ngữ cảnh biên dịch.
Ben Voigt

1
@BenVoigt Chính xác, macro chỉ là sự thay thế văn bản. Trình biên dịch chỉ nhân bản mã, nó không phải là một vấn đề về hiệu suất nhưng có thể làm tăng kích thước chương trình. Đặc biệt đúng trong một số ngữ cảnh mà bạn có giới hạn về kích thước chương trình. Một số mã chứa đầy macro đến mức kích thước của chương trình tăng gấp đôi.
Davide Icardi

1

Macro tiền xử lý không xấu khi chúng được sử dụng cho các mục đích như:

  • Tạo các bản phát hành khác nhau của cùng một phần mềm bằng cách sử dụng loại cấu trúc #ifdef, ví dụ: bản phát hành cửa sổ cho các khu vực khác nhau.
  • Để xác định các giá trị liên quan đến kiểm tra mã.

Giải pháp thay thế- Người ta có thể sử dụng một số loại tệp cấu hình ở định dạng ini, xml, json cho các mục đích tương tự. Nhưng việc sử dụng chúng sẽ có các hiệu ứng thời gian chạy mã mà macro bộ xử lý trước có thể tránh được.


1
vì C ++ 17 constexpr if + một tệp tiêu đề có chứa các biến constexpr "config" có thể thay thế # ifdef's.
Enhex 19/09/19
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.