Constexpr เทียบกับมาโคร


92

ฉันควรใช้มาโครที่ไหนและฉันควรใช้constexprที่ไหน โดยพื้นฐานแล้วพวกเขาไม่เหมือนกันหรือ?

#define MAX_HEIGHT 720

เทียบกับ

constexpr unsigned int max_height = 720;

4
AFAIK constexpr ให้ความปลอดภัยมากกว่า
Code-Apprentice

13
ง่าย: constexr เสมอ
. 'สรรพนาม' ม.

อาจจะตอบคำถามของคุณได้stackoverflow.com/q/4748083/540286
Ortwin Angermeier

คำตอบ:


147

โดยพื้นฐานแล้วพวกเขาไม่เหมือนกันหรือ?

ไม่ไม่อย่างแน่นอน ไม่ได้ใกล้เคียง.

นอกเหนือจากข้อเท็จจริงที่ว่ามาโครของคุณเป็นintและconstexpr unsignedเป็นของคุณunsignedแล้วยังมีข้อแตกต่างที่สำคัญและมาโครมีข้อดีเพียงอย่างเดียว

ขอบเขต

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

#define MAX_HEIGHT 720
constexpr int max_height = 720;

class Window {
  // ...
  int max_height;
};

เป็นเรื่องปกติที่จะมีการเรียกตัวแปรสมาชิกmax_heightเนื่องจากเป็นสมาชิกชั้นเรียนจึงมีขอบเขตที่แตกต่างกันและแตกต่างจากตัวแปรที่ขอบเขตเนมสเปซ หากคุณพยายามใช้ชื่อMAX_HEIGHTของสมาชิกซ้ำตัวประมวลผลก่อนจะเปลี่ยนเป็นเรื่องไร้สาระที่จะไม่รวบรวม:

class Window {
  // ...
  int 720;
};

นี่คือเหตุผลที่คุณต้องให้มาโครUGLY_SHOUTY_NAMESเพื่อให้แน่ใจว่ามันโดดเด่นและคุณสามารถระมัดระวังในการตั้งชื่อเพื่อหลีกเลี่ยงการปะทะกัน หากคุณไม่ใช้มาโครโดยไม่จำเป็นคุณก็ไม่ต้องกังวลเรื่องนั้น (และไม่ต้องอ่านSHOUTY_NAMES)

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

int limit(int height) {
#define MAX_HEIGHT 720
  return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}

เปรียบเทียบกับสิ่งที่สมเหตุสมผลกว่า:

int limit(int height) {
  constexpr int max_height = 720;
  return std::max(height, max_height);
}

ทำไมคุณถึงชอบมาโคร?

ตำแหน่งหน่วยความจำจริง

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

รหัสนี้มีพฤติกรรมที่ไม่ได้กำหนดไว้:

#define MAX_HEIGHT 720
int limit(int height) {
  const int& h = std::max(height, MAX_HEIGHT);
  // ...
  return h;
}

ปัญหาคือนั่นMAX_HEIGHTไม่ใช่ตัวแปรดังนั้นสำหรับการเรียกstd::maxชั่วคราวintจะต้องสร้างโดยคอมไพเลอร์ การอ้างอิงที่ส่งคืนโดยstd::maxอาจอ้างถึงชั่วคราวนั้นซึ่งไม่มีอยู่หลังจากสิ้นสุดคำสั่งนั้นดังนั้นจึงreturn hเข้าถึงหน่วยความจำที่ไม่ถูกต้อง

ปัญหานั้นไม่มีอยู่ในตัวแปรที่เหมาะสมเนื่องจากมีตำแหน่งคงที่ในหน่วยความจำที่ไม่หายไป:

int limit(int height) {
  constexpr int max_height = 720;
  const int& h = std::max(height, max_height);
  // ...
  return h;
}

(ในทางปฏิบัติคุณอาจจะint hไม่ประกาศconst int& hแต่ปัญหาอาจเกิดขึ้นในบริบทที่ละเอียดกว่านี้)

เงื่อนไขของตัวประมวลผลล่วงหน้า

เวลาเดียวที่จะชอบมาโครคือเมื่อคุณต้องการให้พรีโปรเซสเซอร์เข้าใจค่าของมันเพื่อใช้ใน#ifเงื่อนไขเช่น

#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif

คุณไม่สามารถใช้ตัวแปรได้ที่นี่เนื่องจากตัวประมวลผลล่วงหน้าไม่เข้าใจวิธีอ้างถึงตัวแปรตามชื่อ มันเข้าใจเฉพาะสิ่งพื้นฐานพื้นฐานเท่านั้นเช่นการขยายมาโครและคำสั่งที่ขึ้นต้นด้วย#(like #includeand #defineand #if)

หากคุณต้องการค่าคงที่ที่พรีโปรเซสเซอร์สามารถเข้าใจได้คุณควรใช้ตัวประมวลผลล่วงหน้าเพื่อกำหนด หากคุณต้องการค่าคงที่สำหรับรหัส C ++ ปกติให้ใช้รหัส C ++ ปกติ

ตัวอย่างข้างต้นเป็นเพียงการแสดงให้เห็นถึงเงื่อนไขของตัวประมวลผลล่วงหน้า แต่ถึงแม้รหัสนั้นจะหลีกเลี่ยงการใช้ตัวประมวลผลล่วงหน้าได้:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;

3
constexprจำเป็นตัวแปรไม่ใช้หน่วยความจำจนที่อยู่ (ตัวชี้ / อ้างอิง) ถูกนำมา; มิฉะนั้นก็สามารถปรับให้เหมาะสมได้อย่างสมบูรณ์ (และฉันคิดว่าอาจมี Standardese ที่รับประกันได้) ฉันต้องการเน้นย้ำเรื่องนี้เพื่อไม่ให้ผู้คนใช้ ' enumแฮ็ก' แบบเก่าต่อไปจากความคิดที่เข้าใจผิดว่าเรื่องเล็กน้อยconstexprที่ไม่ต้องการพื้นที่เก็บข้อมูลจะครอบครองบางส่วน
underscore_d

3
ส่วน "ตำแหน่งหน่วยความจำจริง" ของคุณไม่ถูกต้อง: 1. คุณกำลังส่งคืนตามค่า (int) ดังนั้นการทำสำเนาชั่วคราวจึงไม่ใช่ปัญหา 2. หากคุณส่งคืนโดยอ้างอิง (int &) คุณint heightก็จะเป็นปัญหาเช่นเดียวกับมาโครเนื่องจากขอบเขตของมันเชื่อมโยงกับฟังก์ชันซึ่งโดยพื้นฐานแล้วชั่วคราวด้วย 3. ความคิดเห็นด้านบน "const int & h จะขยายอายุการใช้งานชั่วคราว" ถูกต้อง
PoweredByRice

4
@underscore_d จริง แต่ไม่ได้เปลี่ยนอาร์กิวเมนต์ ตัวแปรจะไม่ต้องการพื้นที่เก็บข้อมูลเว้นแต่จะมีการใช้งานที่แปลกใหม่ ประเด็นคือเมื่อต้องการตัวแปรจริงพร้อมหน่วยเก็บข้อมูลตัวแปร constexpr จะทำสิ่งที่ถูกต้อง
Jonathan Wakely

1
@PoweredByRice 1. ปัญหาไม่มีส่วนเกี่ยวข้องกับค่าส่งคืนของlimitปัญหาคือค่าส่งคืนของstd::max. 2. ใช่นั่นคือสาเหตุที่ไม่ส่งคืนข้อมูลอ้างอิง 3. ผิดดูลิงค์ coliru ด้านบน
Jonathan Wakely

3
@PoweredByRice ถอนหายใจคุณไม่จำเป็นต้องอธิบายว่า C ++ ทำงานอย่างไรกับฉัน หากคุณมีconst int& h = max(x, y);และmaxส่งคืนตามมูลค่าอายุการใช้งานของมูลค่าที่ส่งคืนจะขยายออกไป ไม่ใช่ตามประเภทผลตอบแทน แต่const int&เป็นประเภทที่ถูกผูกไว้ สิ่งที่ฉันเขียนนั้นถูกต้อง
Jonathan Wakely

11

โดยทั่วไปคุณควรใช้constexprทุกครั้งที่ทำได้และมาโครก็ต่อเมื่อไม่มีวิธีแก้ปัญหาอื่น ๆ

เหตุผล:

มาโครเป็นการแทนที่โค้ดอย่างง่ายและด้วยเหตุนี้จึงมักสร้างความขัดแย้ง (เช่น windows.h maxmacro vs std::max) นอกจากนี้มาโครที่ใช้งานได้ง่ายอาจถูกใช้ในลักษณะอื่นซึ่งจะทำให้เกิดข้อผิดพลาดในการคอมไพล์แปลก ๆ (เช่นQ_PROPERTYใช้กับโครงสร้างสมาชิก)

เนื่องจากความไม่แน่นอนทั้งหมดนี้จึงเป็นรูปแบบรหัสที่ดีในการหลีกเลี่ยงมาโครเหมือนกับที่คุณมักจะหลีกเลี่ยง gotos

constexpr ถูกกำหนดตามความหมายและโดยทั่วไปจะสร้างปัญหาน้อยกว่ามาก


1
การใช้มาโครจะหลีกเลี่ยงไม่ได้ในกรณีใด
Tom Dorone

3
การคอมไพล์ตามเงื่อนไขโดยใช้#ifเช่นสิ่งที่พรีโปรเซสเซอร์มีประโยชน์จริง การกำหนดค่าคงที่ไม่ได้เป็นหนึ่งในสิ่งที่พรีโพรเซสเซอร์จะเป็นประโยชน์สำหรับเว้นแต่ว่าคงต้องเป็นแมโครเพราะมันใช้ในสภาพ preprocessor #ifใช้ ถ้าค่าคงที่สำหรับใช้ในโค้ด C ++ ปกติ (ไม่ใช่คำสั่งพรีโปรเซสเซอร์) ให้ใช้ตัวแปร C ++ ปกติไม่ใช่มาโครตัวประมวลผลล่วงหน้า
Jonathan Wakely

ยกเว้นการใช้มาโครตัวแปรส่วนใหญ่จะใช้มาโครสำหรับสวิตช์คอมไพเลอร์ แต่การพยายามแทนที่คำสั่งแมโครปัจจุบัน (เช่นเงื่อนไขสวิตช์ตามตัวอักษรสตริง) ที่เกี่ยวข้องกับคำสั่งรหัสจริงด้วย constexpr เป็นความคิดที่ดีหรือไม่?

ฉันจะบอกว่าสวิตช์คอมไพเลอร์ไม่ใช่ความคิดที่ดีเช่นกัน อย่างไรก็ตามฉันเข้าใจอย่างถ่องแท้ว่าจำเป็นต้องใช้ในบางครั้ง (เช่นมาโคร) โดยเฉพาะอย่างยิ่งการจัดการกับข้ามแพลตฟอร์มหรือโค้ดฝังตัว เพื่อตอบคำถามของคุณ: หากคุณกำลังติดต่อกับพรีโปรเซสเซอร์อยู่แล้วฉันจะใช้มาโครเพื่อให้ชัดเจนและใช้งานง่ายว่าอะไรคือตัวประมวลผลล่วงหน้าและเวลาในการรวบรวมคืออะไร ฉันขอแนะนำให้แสดงความคิดเห็นอย่างหนักและทำให้การใช้งานสั้นและเป็นภาษาท้องถิ่นมากที่สุด (หลีกเลี่ยงมาโครที่กระจายไปรอบ ๆ หรือ 100 บรรทัด #if) บางทีข้อยกเว้นคือ #ifndef guard ทั่วไป (มาตรฐานสำหรับ #pragma ครั้งเดียว) ซึ่งเป็นที่เข้าใจกันดี
Adrian Maire

3

คำตอบที่ดีโดยJonathon Wakely ฉันขอแนะนำให้คุณดูคำตอบของ jogojapanว่าอะไรคือความแตกต่างระหว่างconstและconstexprก่อนที่คุณจะพิจารณาการใช้มาโคร

มาโครเป็นใบ้ แต่ในทางที่ดี อย่างเห็นได้ชัดในปัจจุบันพวกเขาเป็นตัวช่วยในการสร้างเมื่อคุณต้องการให้ส่วนที่เฉพาะเจาะจงมาก ๆ ของโค้ดของคุณถูกคอมไพล์เมื่อมีการ "กำหนดพารามิเตอร์การสร้าง" โดยปกติทุกที่หมายถึงคือการชื่อแมโครของคุณหรือดีกว่ายังขอเรียกมันTriggerและสิ่งที่เพิ่มชอบ/D:Trigger, -DTriggerฯลฯ เพื่อสร้างเครื่องมือที่ถูกนำมาใช้

แม้ว่าจะมีการใช้งานมาโครที่แตกต่างกันมากมาย แต่สิ่งเหล่านี้คือสองสิ่งที่ฉันเห็นบ่อยที่สุดว่าไม่ใช่แนวทางปฏิบัติที่ไม่ดี / ล้าสมัย:

  1. ส่วนรหัสเฉพาะฮาร์ดแวร์และแพลตฟอร์ม
  2. การสร้างฟุ่มเฟื่อยที่เพิ่มขึ้น

ดังนั้นในขณะที่คุณสามารถบรรลุเป้าหมายเดียวกันในการกำหนด int ด้วยconstexprหรือ a MACROแต่ก็ไม่น่าจะเป็นไปได้ที่ทั้งสองจะทับซ้อนกันเมื่อใช้อนุสัญญาสมัยใหม่ นี่คือการใช้งานมาโครทั่วไปบางส่วนที่ยังไม่ได้ยุติลง

#if defined VERBOSE || defined DEBUG || defined MSG_ALL
    // Verbose message-handling code here
#endif

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

#if defined GEN_3_HW && defined _WIN64
    // Windows-only special handling for 64-bit upcoming hardware
#elif defined GEN_3_HW && defined __APPLE__
    // Special handling for macs on the new hardware
#elif !defined _WIN32 && !defined __linux__ && !defined __APPLE__ && !defined __ANDROID__ && !defined __unix__ && !defined __arm__
    // Greetings, Outlander! ;)
#else
    // Generic handling
#endif
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.