Tại sao các macro không có trong hầu hết các ngôn ngữ lập trình hiện đại?


31

Tôi biết rằng chúng được triển khai cực kỳ không an toàn trong C / C ++. Họ có thể được thực hiện một cách an toàn hơn? Những nhược điểm của macro có thực sự đủ tệ để vượt xa sức mạnh to lớn mà chúng cung cấp không?


4
Chính xác thì sức mạnh nào mà macro cung cấp mà không thể dễ dàng đạt được theo cách khác?
Chinmay Kanchi

2
Trong C #, phải mất một phần mở rộng ngôn ngữ cốt lõi để tạo ra một thuộc tính đơn giản và trường sao lưu thành một khai báo. Và điều này cần nhiều hơn các macro từ vựng: làm thế nào bạn có thể tạo một tính năng tương ứng với công cụ WithEventssửa đổi của Visual Basic ? Bạn sẽ cần một cái gì đó như macro ngữ nghĩa.
Jeffrey Hantin

3
Vấn đề với các macro là, giống như tất cả các cơ chế mạnh mẽ, nó cho phép một lập trình viên phá vỡ các giả định mà người khác đã hoặc sẽ đưa ra. Giả định là chìa khóa cho lý luận và không có khả năng suy luận khả thi về logic, làm cho tiến trình trở nên cấm đoán.
dan_waterworth

2
@Chinmay: macro tạo mã. Không có tính năng nào trong ngôn ngữ Java với sức mạnh đó.
kevin cline

1
@Chinmay Kanchi: Macro cho phép thực thi (đánh giá) mã tại thời gian biên dịch thay vì vào thời gian chạy.
Giorgio

Câu trả lời:


57

Tôi nghĩ lý do chính là các macro là từ vựng . Điều này có một số hậu quả:

  • Trình biên dịch không có cách nào để kiểm tra xem macro có bị đóng về mặt ngữ nghĩa hay không, nghĩa là nó đại diện cho một đơn vị có nghĩa là nghẹt giống như một hàm. (Xem xét #define TWO 1+1- những gì không TWO*TWObằng? 3.)

  • Macro không được gõ như các chức năng. Trình biên dịch không thể kiểm tra xem các tham số và kiểu trả về có ý nghĩa hay không. Nó chỉ có thể kiểm tra biểu thức mở rộng sử dụng macro.

  • Nếu mã không biên dịch, trình biên dịch không có cách nào để biết liệu lỗi nằm ở chính macro hay nơi sử dụng macro. Trình biên dịch sẽ báo cáo sai vị trí một nửa thời gian, hoặc nó phải báo cáo cả hai mặc dù một trong số chúng có thể tốt. (Xem xét #define min(x,y) (((x)<(y))?(x):(y)): Trình biên dịch nên làm gì nếu các loại xykhông khớp hoặc không thực hiện operator<?)

  • Các công cụ tự động không thể làm việc với chúng theo những cách hữu ích về mặt ngữ nghĩa. Cụ thể, bạn không thể có những thứ như IntelliSense cho các macro hoạt động như các hàm nhưng mở rộng thành một biểu thức. (Một lần nữa, minví dụ.)

  • Các tác dụng phụ của một macro không rõ ràng như với các hàm, gây ra sự nhầm lẫn tiềm ẩn cho lập trình viên. (Xem xét lại minví dụ: trong một lệnh gọi hàm, bạn biết rằng biểu thức cho xchỉ được ước tính một lần, nhưng ở đây bạn không thể biết mà không nhìn vào macro.)

Như tôi đã nói, đây là tất cả các hậu quả của thực tế là các macro là từ vựng. Khi bạn cố gắng biến chúng thành một cái gì đó phù hợp hơn, bạn kết thúc với các hàm và hằng số.


32
Không phải tất cả các ngôn ngữ vĩ mô là từ vựng. Ví dụ, macro Scheme là cú pháp và các mẫu C ++ là ngữ nghĩa. Các hệ thống vĩ mô có tính phân biệt rằng chúng có thể được xử lý trên bất kỳ ngôn ngữ nào mà không nhận thức được cú pháp của nó.
Jeffrey Hantin

8
@ Johne Thật không may là Scheme sẽ sử dụng thuật ngữ được tải này cho một cái gì đó khác về cơ bản. Tuy nhiên, các mẫu C ++ không được gọi rộng rãi là macro, có lẽ chính xác là vì chúng không hoàn toàn từ vựng.
Timwi

25
Tôi nghĩ rằng việc sử dụng thuật ngữ macro của Scheme bắt nguồn từ Lisp, điều đó có nghĩa là nó có trước hầu hết các cách sử dụng khác. IIRC hệ thống C / C ++ ban đầu được gọi là bộ tiền xử lý .
Bevan

16
@Bevan nói đúng. Nói macro là từ vựng cũng giống như nói chim không thể bay vì chim bạn quen thuộc nhất là chim cánh cụt. Điều đó nói rằng, hầu hết (nhưng không phải tất cả) các điểm bạn nêu ra cũng áp dụng cho các cú pháp cú pháp, mặc dù có lẽ ở mức độ thấp hơn.
Laurence Gonsalves

13

Nhưng vâng, macro có thể được thiết kế và triển khai tốt hơn trong C / C ++.

Vấn đề với các macro là chúng thực sự là một cơ chế mở rộng cú pháp ngôn ngữ viết lại mã của bạn thành một thứ khác.

  • Trong trường hợp C / C ++, không có kiểm tra độ tỉnh táo cơ bản. Nếu bạn cẩn thận, mọi thứ đều ổn. Nếu bạn mắc lỗi hoặc nếu bạn lạm dụng macro, bạn có thể gặp phải vấn đề lớn.

    Thêm vào đó là nhiều điều đơn giản bạn có thể làm với macro (kiểu C / C ++) có thể được thực hiện theo những cách khác trong các ngôn ngữ khác.

  • Trong các ngôn ngữ khác như các phương ngữ Lisp khác nhau, macro được tích hợp tốt hơn với cú pháp ngôn ngữ cốt lõi, nhưng bạn vẫn có thể gặp vấn đề với các khai báo trong một "rò rỉ" macro. Điều này được giải quyết bằng các macro vệ sinh .


Tóm tắt bối cảnh lịch sử

Macro (viết tắt của hướng dẫn macro) lần đầu tiên xuất hiện trong ngữ cảnh của ngôn ngữ lắp ráp. Theo Wikipedia , macro đã có sẵn trong một số nhà lắp ráp của IBM vào những năm 1950.

LISP ban đầu không có macro, nhưng chúng lần đầu tiên được đưa vào MacLisp vào giữa những năm 1960: https://stackoverflow.com/questions/3065606/when-did-the-idea-of-macros-user-d xác định-code -transifying-xuất hiện . http://www.csee.umbc.edu/cifts/331/resource/ con / Evolution-of-Lisp.pdf . Trước đó, "fexprs" đã cung cấp chức năng giống như macro.

Các phiên bản đầu tiên của C không có macro ( http://cm.bell-labs.com/cm/cs/who/dmr/chist.html ). Chúng được thêm vào khoảng năm 1972-73 thông qua bộ tiền xử lý. Trước đó, C chỉ hỗ trợ #include#define.

Bộ tiền xử lý macro M4 có nguồn gốc từ khoảng năm 1977.

Nhiều ngôn ngữ gần đây rõ ràng thực hiện các macro trong đó mô hình hoạt động là cú pháp hơn là văn bản.

Vì vậy, khi ai đó nói về tính ưu việt của một định nghĩa cụ thể của thuật ngữ "vĩ mô", điều quan trọng cần lưu ý là ý nghĩa đã phát triển theo thời gian.


9
C ++ được ban phước (?) Với các hệ thống macro TWO: bộ tiền xử lý và mở rộng mẫu.
Jeffrey Hantin

Tôi nghĩ rằng tuyên bố rằng mở rộng mẫu là một macro đang kéo dài định nghĩa của macro. Sử dụng định nghĩa của bạn làm cho câu hỏi ban đầu trở nên vô nghĩa và hoàn toàn sai, vì hầu hết các ngôn ngữ hiện đại trên thực tế đều có macro (theo định nghĩa của bạn). Tuy nhiên, sử dụng macro như phần lớn các nhà phát triển sẽ sử dụng nó làm cho nó trở thành một câu hỏi hay.
Dunk

@Dunk, định nghĩa về một macro như trong Lisp có trước sự hiểu biết ngu ngốc "hiện đại" như trong bộ tiền xử lý C thảm hại.
SK-logic

@SK: Tốt hơn là "đúng" về mặt kỹ thuật và làm xáo trộn cuộc trò chuyện hay tốt hơn là nên hiểu?
Dunk

1
@Dunk, thuật ngữ không thuộc sở hữu của đám dốt. Sự thiếu hiểu biết của họ không bao giờ nên được xem xét, nếu không nó sẽ dẫn đến làm giảm toàn bộ lĩnh vực khoa học máy tính. Nếu ai đó hiểu "macro" chỉ như một tham chiếu đến bộ tiền xử lý C, thì không có gì ngoài sự thiếu hiểu biết. Tôi nghi ngờ có một tỷ lệ đáng kể những người chưa bao giờ nghe nói về Lisp hoặc thậm chí, pardonnez mon français, VBA "macro" trong Office.
SK-logic

12

Macro có thể, như Scott lưu ý , cho phép bạn ẩn logic. Tất nhiên, các chức năng, lớp, thư viện và nhiều thiết bị phổ biến khác cũng vậy.

Nhưng một hệ thống vĩ mô mạnh mẽ có thể đi xa hơn, cho phép bạn thiết kế và sử dụng cú pháp và các cấu trúc không thường thấy trong ngôn ngữ. Đây thực sự có thể là một công cụ tuyệt vời: ngôn ngữ dành riêng cho tên miền, trình tạo mã và hơn thế nữa, tất cả đều nằm trong sự thoải mái của một môi trường ngôn ngữ duy nhất ...

Tuy nhiên, nó có thể bị lạm dụng. Nó có thể làm cho mã khó đọc, hiểu và gỡ lỗi hơn, tăng thời gian cần thiết cho các lập trình viên mới làm quen với một cơ sở mã và dẫn đến những sai lầm và sự chậm trễ tốn kém.

Vì vậy, đối với các ngôn ngữ nhằm đơn giản hóa việc lập trình (như Java hoặc Python), một hệ thống như vậy là một sự vô cảm.


3
Và sau đó Java đã đi ra khỏi đường ray bằng cách thêm foreach, chú thích, khẳng định,
khái quát

2
@Jeffrey: và đừng quên, rất nhiều trình tạo mã của bên thứ 3 . Suy nghĩ thứ hai, hãy quên những điều đó.
Shog9

4
Một ví dụ khác về cách Java đơn giản thay vì đơn giản.
Jeffrey Hantin

3
@JeffreyHantin: Điều gì sai với foreach?
Casebash

1
@Dunk Sự tương tự này có thể là một sự kéo dài ... nhưng tôi nghĩ khả năng mở rộng ngôn ngữ là loại giống như plutoni. Chi phí để thực hiện, rất khó để biến thành các hình dạng mong muốn và nguy hiểm đáng kể nếu sử dụng sai, nhưng cũng mạnh mẽ đáng kinh ngạc khi được áp dụng tốt.
Jeffrey Hantin

6

Macro có thể được triển khai rất an toàn trong một số trường hợp - ví dụ trong Lisp, macro chỉ là các hàm trả về mã được chuyển đổi dưới dạng cấu trúc dữ liệu (biểu thức s). Tất nhiên, Lisp được hưởng lợi đáng kể từ thực tế rằng nó là đồng âm và thực tế là "mã là dữ liệu".

Một ví dụ về cách các macro có thể dễ dàng là ví dụ Clojure này chỉ định một giá trị mặc định sẽ được sử dụng trong trường hợp ngoại lệ:

(defmacro on-error [default-value code]
  `(try ~code (catch Exception ~'e ~default-value)))

(on-error 0 (+ nil nil))               ;; would normally throw NullPointerException
=> 0                                   ;l; but we get the default value

Ngay cả trong Lisps, lời khuyên chung là "không sử dụng macro trừ khi bạn phải".

Nếu bạn không sử dụng ngôn ngữ đồng âm, thì macro sẽ phức tạp hơn nhiều và các tùy chọn khác đều có một số sai lầm:

  • Các macro dựa trên văn bản - ví dụ: bộ tiền xử lý C - đơn giản để thực hiện nhưng rất khó sử dụng chính xác vì bạn cần tạo cú pháp nguồn chính xác ở dạng văn bản, bao gồm bất kỳ quirks cú pháp nào
  • DSLS dựa trên macro - ví dụ: hệ thống mẫu C ++. Phức tạp, bản thân nó có thể dẫn đến một số cú pháp khó, có thể cực kỳ phức tạp đối với người viết trình biên dịch và công cụ để xử lý chính xác vì nó đưa ra sự phức tạp mới đáng kể vào cú pháp ngôn ngữ và ngữ nghĩa.
  • API thao tác AST / mã byte - ví dụ: tạo phản xạ / tạo mã byte Java - về mặt lý thuyết rất linh hoạt nhưng có thể rất dài dòng: nó có thể yêu cầu rất nhiều mã để làm những việc khá đơn giản. Nếu phải mất mười dòng mã để tạo tương đương với hàm ba dòng, thì bạn đã không đạt được nhiều với nỗ lực lập trình meta của mình ...

Hơn nữa, tất cả mọi thứ mà một macro có thể làm cuối cùng có thể đạt được theo một cách khác bằng một ngôn ngữ hoàn chỉnh (ngay cả khi điều này có nghĩa là viết rất nhiều bản tóm tắt). Do tất cả sự khó khăn này, không có gì đáng ngạc nhiên khi nhiều ngôn ngữ quyết định rằng macro không thực sự xứng đáng với tất cả nỗ lực để thực hiện.


Ừ Không có nhiều "mã là dữ liệu là một điều tốt" rác. Mã là mã, dữ liệu là dữ liệu và việc không phân tách đúng hai thứ này là một lỗ hổng bảo mật. Bạn sẽ nghĩ rằng sự xuất hiện của SQL Injection là một trong những lớp dễ bị tổn thương lớn nhất trong sự tồn tại sẽ làm mất uy tín của ý tưởng tạo mã và dữ liệu có thể dễ dàng thay thế cho nhau.
Mason Wheeler

10
@Mason - Tôi không nghĩ bạn hiểu khái niệm mã-dữ liệu. Tất cả mã nguồn chương trình C cũng là dữ liệu - nó chỉ được thể hiện dưới dạng văn bản. Lisps là như nhau, ngoại trừ chúng thể hiện mã trong các cấu trúc dữ liệu trung gian thực tế (biểu thức s) cho phép nó được thao tác và biến đổi bởi các macro trước khi biên dịch. Trong cả hai trường hợp, việc gửi đầu vào không đáng tin cậy cho trình biên dịch có thể là một lỗ hổng bảo mật - nhưng điều đó thật khó để vô tình và đó là lỗi của bạn khi làm điều gì đó ngu ngốc, không phải là trình biên dịch.
mikera

Rust là một hệ thống vĩ mô an toàn thú vị khác. Nó có các quy tắc / ngữ nghĩa nghiêm ngặt để tránh các loại vấn đề liên quan đến macro từ vựng C / C ++ và sử dụng DSL để thu các phần của biểu thức đầu vào thành các biến macro để chèn vào sau. Nó không mạnh bằng khả năng của Lisps để gọi bất kỳ chức năng nào trên đầu vào, nhưng nó cung cấp một tập hợp lớn các macro thao tác hữu ích có thể được gọi từ các macro khác.
zstewart

Ừ Không thêm rác bảo mật . Mặt khác, đổ lỗi cho mọi hệ thống có kiến ​​trúc von Neumann tích hợp, ví dụ mọi bộ xử lý IA-32 có khả năng tự sửa đổi mã. Tôi nghi ngờ liệu bạn có thể lấp đầy lỗ hổng về mặt vật lý hay không ... Dù sao đi nữa, bạn phải đối mặt với thực tế nói chung: sự đồng hình giữa mã và dữ liệu là bản chất của thế giới theo nhiều cách . Và đó là (có lẽ) bạn, lập trình viên, có nhiệm vụ duy trì sự bất biến của các yêu cầu bảo mật, điều này không giống với việc áp dụng cách ly sớm một cách giả tạo ở bất cứ đâu.
FrankHB

4

Để trả lời câu hỏi của bạn, hãy suy nghĩ về các macro được sử dụng chủ yếu cho mục đích gì (Cảnh báo: mã do não biên soạn).

  • Macro được sử dụng để xác định các hằng số tượng trưng #define X 100

Điều này có thể dễ dàng được thay thế bằng: const int X = 100;

  • Các macro được sử dụng để xác định (về cơ bản) các hàm không xác định kiểu nội tuyến #define max(X,Y) (X>Y?X:Y)

Trong bất kỳ ngôn ngữ nào hỗ trợ nạp chồng hàm, điều này có thể được mô phỏng theo cách an toàn hơn nhiều loại bằng cách sử dụng các hàm quá tải đúng loại, hoặc, trong ngôn ngữ hỗ trợ tổng quát, bởi một hàm chung. Macro sẽ vui vẻ cố gắng so sánh bất cứ thứ gì kể cả con trỏ hoặc chuỗi, có thể biên dịch, nhưng gần như chắc chắn không phải là thứ bạn muốn. Mặt khác, nếu bạn thực hiện các loại macro an toàn, chúng không mang lại lợi ích hoặc sự thuận tiện cho các chức năng quá tải.

  • Macro được sử dụng để chỉ định các phím tắt cho các yếu tố thường được sử dụng. #define p printf

Điều này dễ dàng được thay thế bởi một chức năng p()làm điều tương tự. Điều này khá liên quan đến C (yêu cầu bạn sử dụng va_arg()họ hàm) nhưng trong nhiều ngôn ngữ khác hỗ trợ số lượng biến đối số của hàm, nó đơn giản hơn nhiều.

Hỗ trợ các tính năng này trong một ngôn ngữ thay vì ngôn ngữ macro đặc biệt đơn giản hơn, ít bị lỗi hơn và ít gây nhầm lẫn hơn cho những người khác đọc mã. Trên thực tế, tôi không thể nghĩ về một trường hợp sử dụng duy nhất cho các macro không thể dễ dàng bị trùng lặp theo một cách khác. Nơi duy nhất mà macro thực sự hữu ích là khi chúng được gắn với các cấu trúc biên dịch có điều kiện như #if(v.v.).

Vào thời điểm đó, tôi sẽ không tranh luận với bạn, vì tôi tin rằng các giải pháp không tiền xử lý cho việc biên dịch có điều kiện trong các ngôn ngữ phổ biến là cực kỳ cồng kềnh (như tiêm mã byte trong Java). Nhưng các ngôn ngữ như D đã đưa ra các giải pháp không yêu cầu bộ xử lý trước và không cồng kềnh hơn so với sử dụng các điều kiện tiền xử lý, trong khi ít bị lỗi hơn.


1
nếu bạn phải #define max, vui lòng đặt dấu ngoặc quanh các tham số, để không có hiệu ứng bất ngờ từ ưu tiên toán tử ... như #define max (X, Y) ((X)> (Y)? (X) :( Y))
foo

Bạn có nhận ra rằng đó chỉ là một ví dụ ... Mục đích là để minh họa.
Chinmay Kanchi

+1 cho việc xử lý có hệ thống này. Tôi muốn thêm rằng việc biên dịch có điều kiện có thể dễ dàng trở thành một cơn ác mộng - tôi nhớ rằng có một chương trình tên là "unifdef" (??) với mục đích là làm cho nó hiển thị những gì còn lại sau khi xử lý hậu kỳ.
Ingo

7
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way: trong C ít nhất, bạn không thể tạo định danh (tên biến) bằng cách sử dụng nối mã thông báo mà không sử dụng macro.
Charles Salvia

Các macro cho phép đảm bảo rằng các cấu trúc dữ liệu và mã nhất định vẫn "song song". Ví dụ: nếu có một số lượng nhỏ các điều kiện với các thông điệp được liên kết và người ta sẽ cần duy trì chúng ở định dạng ngắn gọn, cố gắng sử dụng một enumđể xác định các điều kiện và một chuỗi chuỗi không đổi để xác định các thông báo có thể dẫn đến sự cố nếu enum và mảng không đồng bộ. Sử dụng một macro để xác định tất cả các cặp (enum, chuỗi) và sau đó bao gồm macro đó hai lần với các định nghĩa đúng khác trong phạm vi mỗi lần sẽ cho phép một giá trị enum bên cạnh chuỗi của nó.
supercat

2

Vấn đề lớn nhất mà tôi gặp phải với các macro là khi được sử dụng nhiều, chúng có thể khiến mã rất khó đọc và duy trì vì chúng cho phép bạn ẩn logic trong macro có thể dễ dàng tìm thấy (và có thể hoặc không tầm thường ).


Không nên macro được ghi lại?
Casebash

@Casebash: Tất nhiên, macro nên được ghi lại giống như bất kỳ mã nguồn nào khác ... nhưng trong thực tế, tôi hiếm khi thấy nó được thực hiện.
Scott Dorman

3
Bao giờ gỡ lỗi tài liệu? Nó chỉ là không đủ khi xử lý mã viết kém.
JeffO

OOP cũng có một số vấn đề như vậy ...
aoeu256

2

Hãy bắt đầu bằng cách lưu ý rằng MACRO trong C / C ++ rất hạn chế, dễ bị lỗi và không thực sự hữu ích.

MACRO như được thực hiện trong LISP, hoặc ngôn ngữ trình biên dịch z / OS có thể đáng tin cậy và cực kỳ hữu ích.

Nhưng vì lạm dụng chức năng hạn chế trong C, họ đã thu thập được tiếng xấu. Vì vậy, không ai thực hiện các macro nữa, thay vào đó bạn có được những thứ như Mẫu làm một số macro công cụ đơn giản được sử dụng để làm và những thứ như chú thích của Java mà một số macro công cụ phức tạp hơn thường làm.

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.