มาโครเทียบกับฟังก์ชันในค


104

ฉันมักจะเห็นตัวอย่างและกรณีที่การใช้มาโครดีกว่าการใช้ฟังก์ชัน

ใครช่วยอธิบายตัวอย่างข้อเสียของมาโครเทียบกับฟังก์ชันได้ไหม


21
เปิดคำถามในหัว มาโครในสถานการณ์ใดดีกว่ากัน? ใช้ฟังก์ชันจริงเว้นแต่คุณจะแสดงให้เห็นว่ามาโครดีกว่า
David Heffernan

คำตอบ:


115

มาโครมีข้อผิดพลาดได้ง่ายเนื่องจากอาศัยการแทนที่ข้อความและไม่ทำการตรวจสอบประเภท ตัวอย่างเช่นมาโครนี้:

#define square(a) a * a

ทำงานได้ดีเมื่อใช้กับจำนวนเต็ม:

square(5) --> 5 * 5 --> 25

แต่ทำสิ่งที่แปลกมากเมื่อใช้กับนิพจน์:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

การใส่วงเล็บรอบอาร์กิวเมนต์ช่วยได้ แต่ไม่สามารถขจัดปัญหาเหล่านี้ได้ทั้งหมด

เมื่อมาโครมีหลายคำสั่งคุณอาจมีปัญหากับโครงสร้างโฟลว์การควบคุม:

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

กลยุทธ์ปกติในการแก้ไขปัญหานี้คือการใส่ข้อความไว้ในลูป "do {... } while (0)"

หากคุณมีสองโครงสร้างที่มีเขตข้อมูลที่มีชื่อเดียวกัน แต่มีความหมายต่างกันแมโครเดียวกันอาจใช้ได้กับทั้งสองอย่างโดยมีผลลัพธ์แปลก ๆ :

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

ในที่สุดมาโครอาจแก้ไขจุดบกพร่องได้ยากทำให้เกิดข้อผิดพลาดทางไวยากรณ์แปลก ๆ หรือข้อผิดพลาดรันไทม์ที่คุณต้องขยายเพื่อทำความเข้าใจ (เช่นด้วย gcc -E) เนื่องจากดีบักเกอร์ไม่สามารถผ่านมาโครได้ดังตัวอย่างนี้:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

ฟังก์ชันและค่าคงที่แบบอินไลน์ช่วยหลีกเลี่ยงปัญหาเหล่านี้กับมาโครได้มากมาย แต่ก็ไม่สามารถใช้ได้เสมอไป ในกรณีที่ใช้มาโครเพื่อระบุพฤติกรรมหลายรูปแบบโดยเจตนาความแตกต่างโดยไม่ตั้งใจอาจหลีกเลี่ยงได้ยาก C ++ มีคุณสมบัติมากมายเช่นเทมเพลตที่ช่วยสร้างโครงสร้างโพลีมอร์ฟิกที่ซับซ้อนในรูปแบบที่ปลอดภัยโดยไม่ต้องใช้มาโคร ดูรายละเอียดในภาษาการเขียนโปรแกรม C ++ของ Stroustrup


44
โฆษณา C ++ คืออะไร?
Pacerier

4
เห็นด้วยนี่เป็นคำถาม C ไม่จำเป็นต้องเพิ่มอคติ
ideasman42

17
C ++ เป็นส่วนขยายของ C ที่เพิ่มคุณสมบัติ (เหนือสิ่งอื่นใด) ที่มีจุดมุ่งหมายเพื่อแก้ไขข้อ จำกัด เฉพาะของ C ฉันไม่ใช่แฟนของ C ++ แต่ฉันคิดว่ามันเป็นหัวข้อที่นี่
D Coetzee

1
มักใช้มาโครฟังก์ชันอินไลน์และเทมเพลตเพื่อเพิ่มประสิทธิภาพ พวกเขาใช้งานมากเกินไปและมีแนวโน้มที่จะทำร้ายประสิทธิภาพเนื่องจากการขยายตัวของโค้ดซึ่งจะลดประสิทธิภาพของแคชคำสั่ง CPU เราสามารถสร้างโครงสร้างข้อมูลทั่วไปใน C ได้อย่างรวดเร็วโดยไม่ต้องใช้เทคนิคเหล่านี้
Sam Watkins

1
ตามมาตรฐาน ISO / IEC 9899: 1999 §6.5.1 "ระหว่างจุดลำดับก่อนหน้าและลำดับถัดไปวัตถุจะต้องมีการแก้ไขค่าที่เก็บไว้มากที่สุดในครั้งเดียวโดยการประเมินนิพจน์" (ถ้อยคำที่คล้ายกันมีอยู่ในมาตรฐาน C ก่อนหน้าและตามมา) ดังนั้นนิพจน์x++*x++จึงไม่สามารถบอกได้ว่าเพิ่มขึ้นxสองครั้ง มันเรียกใช้พฤติกรรมที่ไม่ได้กำหนดซึ่งหมายความว่าคอมไพเลอร์มีอิสระที่จะทำทุกอย่างที่ต้องการ - มันอาจเพิ่มขึ้นxสองครั้งหรือครั้งเดียวหรือไม่ทำเลยก็ได้ มันอาจจะยกเลิกการมีข้อผิดพลาดหรือแม้กระทั่งทำให้ปีศาจบินออกจากจมูกของคุณ
Psychonaut

42

คุณสมบัติมาโคร :

  • มาโครถูกประมวลผลล่วงหน้า
  • ไม่มีการตรวจสอบประเภท
  • ความยาวรหัสเพิ่มขึ้น
  • การใช้มาโครอาจทำให้เกิดผลข้างเคียง
  • ความเร็วในการดำเนินการเร็วขึ้น
  • ก่อนที่ชื่อแมโครคอมไพล์จะถูกแทนที่ด้วยค่าแมโคร
  • มีประโยชน์เมื่อโค้ดขนาดเล็กปรากฏขึ้นหลายครั้ง
  • มาโครไม่ตรวจสอบข้อผิดพลาดในการคอมไพล์

คุณสมบัติของฟังก์ชัน :

  • มีการรวบรวมฟังก์ชัน
  • การตรวจสอบประเภทเสร็จสิ้น
  • ความยาวรหัสยังคงเหมือนเดิม
  • ไม่มีผลข้างเคียง
  • ความเร็วในการดำเนินการช้าลง
  • ระหว่างการเรียกใช้ฟังก์ชันการโอนการควบคุมจะเกิดขึ้น
  • มีประโยชน์เมื่อโค้ดขนาดใหญ่ปรากฏขึ้นหลายครั้ง
  • ฟังก์ชันตรวจสอบข้อผิดพลาดในการคอมไพล์

2
"ความเร็วในการดำเนินการเร็วกว่า" จำเป็นต้องมีการอ้างอิง คอมไพเลอร์ที่ค่อนข้างมีความสามารถใด ๆ ในทศวรรษที่ผ่านมาจะใช้ฟังก์ชันอินไลน์ได้ดีหากคิดว่ามันจะให้ประโยชน์ด้านประสิทธิภาพ
Voo

1
ไม่ว่าในบริบทของการประมวลผล MCU ระดับต่ำ (AVRs เช่น ATMega32) มาโครเป็นตัวเลือกที่ดีกว่าเนื่องจากไม่ได้เพิ่มจำนวนการโทรเช่นการเรียกใช้ฟังก์ชัน
hardyVeles

1
@hardyVeles ไม่อย่างนั้น คอมไพเลอร์แม้กระทั่ง AVR ก็สามารถอินไลน์โค้ดได้อย่างชาญฉลาด นี่คือตัวอย่างgodbolt.org/z/Ic21iM
Edward

34

ผลข้างเคียงเป็นเรื่องใหญ่ นี่เป็นกรณีทั่วไป:

#define min(a, b) (a < b ? a : b)

min(x++, y)

ขยายเป็น:

(x++ < y ? x++ : y)

xได้รับการเพิ่มขึ้นสองครั้งในคำสั่งเดียวกัน (และพฤติกรรมที่ไม่ได้กำหนด)


การเขียนมาโครหลายบรรทัดก็เป็นความเจ็บปวดเช่นกัน:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

พวกเขาต้องการ\ที่ส่วนท้ายของแต่ละบรรทัด


มาโครไม่สามารถ "ส่งคืน" อะไรได้เว้นแต่คุณจะทำให้เป็นนิพจน์เดียว:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

ไม่สามารถทำได้ในมาโครเว้นแต่คุณจะใช้คำสั่งนิพจน์ของ GCC (แก้ไข: คุณสามารถใช้ตัวดำเนินการลูกน้ำแม้ว่า ... มองข้ามสิ่งนั้นไป ... แต่อาจยังอ่านได้น้อยกว่า)


ลำดับปฏิบัติการ: (เอื้อเฟื้อโดย @ouah)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

ขยายเป็น:

(x & 0xFF < 42 ? x & 0xFF : 42)

แต่&มีความสำคัญต่ำกว่า<. ดังนั้น0xFF < 42ได้รับการประเมินก่อน


5
และการไม่ใส่วงเล็บที่มีอาร์กิวเมนต์มาโครในนิยามมาโครอาจทำให้เกิดปัญหาลำดับความสำคัญเช่นmin(a & 0xFF, 42)
ouah

อ่าใช่ ไม่เห็นความคิดเห็นของคุณในขณะที่ฉันกำลังอัปเดตโพสต์ ฉันเดาว่าฉันจะพูดถึงเรื่องนั้นด้วย
Mysticial

14

ตัวอย่างที่ 1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

ในขณะที่:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

ตัวอย่างที่ 2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

เปรียบเทียบกับ:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}

13

หากมีข้อสงสัยให้ใช้ฟังก์ชัน (หรือฟังก์ชันอินไลน์)

อย่างไรก็ตามคำตอบส่วนใหญ่จะอธิบายถึงปัญหาเกี่ยวกับมาโครแทนที่จะมองง่ายๆว่ามาโครเป็นสิ่งชั่วร้ายเพราะอาจเกิดอุบัติเหตุโง่ ๆ ได้
คุณสามารถตระหนักถึงข้อผิดพลาดและเรียนรู้ที่จะหลีกเลี่ยง จากนั้นใช้มาโครเมื่อมีเหตุผลที่ดีเท่านั้น

มีบางกรณีพิเศษที่มีข้อดีในการใช้มาโคร ได้แก่ :

  • ฟังก์ชันทั่วไปดังที่ระบุไว้ด้านล่างคุณสามารถมีมาโครที่ใช้กับอาร์กิวเมนต์อินพุตประเภทต่างๆได้
  • จำนวนตัวแปรของการขัดแย้งสามารถแมฟังก์ชั่นที่แตกต่างกันแทนการใช้ของ va_argsC
    เช่น: https://stackoverflow.com/a/24837037/432509
  • พวกเขาสามารถเลือกที่จะมีข้อมูลในท้องถิ่นเช่นสายการแก้ปัญหา:
    ( __FILE__, __LINE__, __func__) ตรวจสอบเงื่อนไขก่อน / หลังassertความล้มเหลวหรือแม้กระทั่งการยืนยันแบบคงที่ดังนั้นโค้ดจะไม่รวบรวมเมื่อใช้งานที่ไม่เหมาะสม (ส่วนใหญ่มีประโยชน์สำหรับการสร้างดีบัก)
  • ตรวจสอบ args ป้อนข้อมูลคุณสามารถทำแบบทดสอบใน args การป้อนข้อมูลเช่นการตรวจสอบชนิดของพวกเขา sizeof ตรวจสอบstructสมาชิกที่มีอยู่ก่อนที่จะหล่อ
    (จะมีประโยชน์สำหรับประเภท polymorphic)
    หรือตรวจสอบว่าอาร์เรย์ตรงตามเงื่อนไขความยาวบางประการ
    ดู: https://stackoverflow.com/a/29926435/432509
  • ในขณะที่ระบุว่าฟังก์ชันทำการตรวจสอบประเภท C ก็จะบังคับค่าด้วยเช่นกัน (เช่น ints / floats) ในบางกรณีอาจเป็นปัญหาได้ เป็นไปได้ที่จะเขียนมาโครซึ่งมีความแม่นยำมากกว่าฟังก์ชันเกี่ยวกับอาร์กิวเมนต์อินพุต ดู: https://stackoverflow.com/a/25988779/432509
  • การใช้งานเป็นเครื่องห่อหุ้มฟังก์ชันในบางกรณีคุณอาจต้องการหลีกเลี่ยงการใช้ตัวเองซ้ำเช่น ... func(FOO, "FOO");คุณสามารถกำหนดมาโครที่ขยายสตริงให้คุณfunc_wrapper(FOO);
  • เมื่อคุณต้องการจัดการตัวแปรในขอบเขตโลคัลของผู้โทรการส่งตัวชี้ไปยังตัวชี้จะทำงานได้ดีตามปกติ แต่ในบางกรณีการใช้มาโครยังคงมีปัญหาน้อยกว่า
    (ที่ได้รับมอบหมายกับตัวแปรหลายสำหรับการดำเนินงานต่อพิกเซลเป็นตัวอย่างคุณอาจต้องการแมโครมากกว่าฟังก์ชั่น ... แม้ว่ามันจะยังคงขึ้นอยู่มากกับบริบทเนื่องจากinlineฟังก์ชั่นอาจเป็นตัวเลือก)

เป็นที่ยอมรับว่าบางส่วนอาศัยส่วนขยายของคอมไพเลอร์ซึ่งไม่ใช่มาตรฐาน C ซึ่งหมายความว่าคุณอาจมีโค้ดแบบพกพาน้อยกว่าหรือต้องใช้โค้ดifdefเหล่านี้ดังนั้นจึงถูกใช้ประโยชน์จากเมื่อคอมไพเลอร์รองรับเท่านั้น


หลีกเลี่ยงการสร้างอินสแตนซ์อาร์กิวเมนต์หลายรายการ

สังเกตนี้ตั้งแต่หนึ่งของสาเหตุที่พบบ่อยที่สุดของความผิดพลาดในแมโคร(ผ่านในx++เช่นที่แมโครอาจเพิ่มขึ้นหลายครั้ง)

เป็นไปได้ที่จะเขียนมาโครที่หลีกเลี่ยงผลข้างเคียงด้วยการโต้ตอบหลาย ๆ ครั้ง

C11 ทั่วไป

หากคุณต้องการมีsquareมาโครที่ใช้ได้กับประเภทต่างๆและรองรับ C11 คุณสามารถทำได้ ...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

นิพจน์งบ

นี้เป็นส่วนขยายของคอมไพเลอร์สนับสนุนโดย GCC, เสียงดังกราว, EKOPath อินเทล c ++ ( แต่ไม่ MSVC) ;

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

ดังนั้นข้อเสียของมาโครคือคุณจำเป็นต้องรู้เพื่อใช้สิ่งเหล่านี้เพื่อเริ่มต้นและไม่ได้รับการสนับสนุนอย่างกว้างขวาง

ประโยชน์อย่างหนึ่งคือในกรณีนี้คุณสามารถใช้squareฟังก์ชันเดียวกันได้หลายประเภท


1
"... ได้รับการสนับสนุนอย่างกว้างขวาง .. "ฉันคิดว่าการแสดงออกของคำสั่งที่คุณกล่าวถึงไม่ได้รับการสนับสนุนโดย cl.exe? (MS's Compiler)
gideon

1
@gideon คำตอบที่ถูกแก้ไขแม้ว่าสำหรับแต่ละคุณลักษณะที่กล่าวถึงไม่แน่ใจว่าจำเป็นต้องมีเมทริกซ์การสนับสนุนคอมไพเลอร์ - คุณลักษณะบางอย่าง
ideasman42

12

ไม่มีการตรวจสอบประเภทของพารามิเตอร์และโค้ดซ้ำซึ่งอาจนำไปสู่การขยายโค้ด ไวยากรณ์มาโครยังสามารถนำไปสู่กรณีขอบแปลก ๆ จำนวนเท่าใดก็ได้ที่กึ่งโคลอนหรือลำดับความสำคัญสามารถเข้ามาขวางทางได้ นี่คือลิงค์ที่แสดงให้เห็นถึงความชั่วร้ายของมาโคร


6

ข้อเสียเปรียบประการหนึ่งของมาโครคือดีบักเกอร์อ่านซอร์สโค้ดซึ่งไม่มีมาโครขยายดังนั้นการเรียกใช้ดีบักเกอร์ในมาโครจึงไม่จำเป็นต้องมีประโยชน์ ไม่จำเป็นต้องพูดว่าคุณไม่สามารถตั้งค่าเบรกพอยต์ภายในมาโครได้เช่นเดียวกับที่คุณสามารถทำได้ด้วยฟังก์ชัน


จุดพักเป็นข้อตกลงที่สำคัญมากขอบคุณที่ชี้ให้เห็น
Hans


6

กำลังเพิ่มคำตอบนี้ ..

มาโครจะถูกแทนที่โดยตรงในโปรแกรมโดยพรีโปรเซสเซอร์ (เนื่องจากโดยพื้นฐานแล้วมันเป็นคำสั่งของตัวประมวลผลล่วงหน้า) ดังนั้นพวกเขาจึงใช้พื้นที่หน่วยความจำมากกว่าฟังก์ชันที่เกี่ยวข้องอย่างหลีกเลี่ยงไม่ได้ ในทางกลับกันฟังก์ชันต้องใช้เวลามากขึ้นในการเรียกใช้และเพื่อให้ผลลัพธ์กลับมาและค่าใช้จ่ายนี้สามารถหลีกเลี่ยงได้โดยใช้มาโคร

มาโครยังมีเครื่องมือพิเศษบางอย่างที่สามารถช่วยในการเคลื่อนย้ายโปรแกรมบนแพลตฟอร์มต่างๆ

มาโครไม่จำเป็นต้องกำหนดชนิดข้อมูลสำหรับอาร์กิวเมนต์ซึ่งแตกต่างจากฟังก์ชัน

โดยรวมแล้วเป็นเครื่องมือที่มีประโยชน์ในการเขียนโปรแกรม และสามารถใช้ทั้งคำแนะนำและฟังก์ชันมาโครได้ขึ้นอยู่กับสถานการณ์


3

ฉันไม่สังเกตเห็นในคำตอบข้างต้นข้อดีอย่างหนึ่งของฟังก์ชันเหนือมาโครที่ฉันคิดว่าสำคัญมาก:

ฟังก์ชันสามารถส่งผ่านเป็นอาร์กิวเมนต์มาโครไม่สามารถ

ตัวอย่างที่เป็นรูปธรรม: คุณต้องการเขียนเวอร์ชันทางเลือกของฟังก์ชัน 'strpbrk' มาตรฐานที่จะยอมรับแทนที่จะเป็นรายการอักขระที่ชัดเจนเพื่อค้นหาภายในสตริงอื่นฟังก์ชัน (ตัวชี้ไปยัง a) ที่จะส่งกลับ 0 จนกว่าอักขระจะเป็น พบว่าผ่านการทดสอบบางอย่าง (กำหนดโดยผู้ใช้) เหตุผลหนึ่งที่คุณอาจต้องการทำเช่นนี้ก็เพื่อให้คุณสามารถใช้ประโยชน์จากฟังก์ชันไลบรารีมาตรฐานอื่น ๆ ได้: แทนที่จะจัดเตรียมสตริงที่ชัดเจนเต็มไปด้วยเครื่องหมายวรรคตอนคุณสามารถส่ง 'ispunct' ของ ctype.h แทนได้เป็นต้นหากใช้ 'ispunct' เป็น มาโครนี้จะใช้ไม่ได้

มีตัวอย่างอื่น ๆ อีกเพียบ ตัวอย่างเช่นหากการเปรียบเทียบของคุณทำได้โดยมาโครแทนที่จะเป็นฟังก์ชันคุณจะไม่สามารถส่งต่อไปยัง 'qsort' ของ stdlib.h ได้

สถานการณ์ที่คล้ายคลึงกันใน Python คือ 'พิมพ์' ในเวอร์ชัน 2 เทียบกับเวอร์ชัน 3 (คำสั่งที่ไม่สามารถส่งผ่านเทียบกับฟังก์ชันที่ผ่านได้)


1
ขอบคุณสำหรับคำตอบนี้
Kyrol

1

หากคุณส่งฟังก์ชันเป็นอาร์กิวเมนต์ไปยังมาโครระบบจะประเมินทุกครั้ง ตัวอย่างเช่นหากคุณเรียกหนึ่งในมาโครยอดนิยม:

#define MIN(a,b) ((a)<(b) ? (a) : (b))

เช่นนั้น

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime จะได้รับการประเมิน 5 ครั้งซึ่งสามารถลดประสิทธิภาพลงได้อย่างมาก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.