จุลภาคในมาโคร C / C ++


104

สมมติว่าเรามีมาโครแบบนี้

#define FOO(type,name) type name

ซึ่งเราสามารถใช้เช่น

FOO(int, int_var);

แต่ไม่ง่ายอย่างนั้นเสมอไป:

FOO(std::map<int, int>, map_var); // error: macro "FOO" passed 3 arguments, but takes just 2

แน่นอนเราทำได้:

 typedef std::map<int, int> map_int_int_t;
 FOO(map_int_int_t, map_var); // OK

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


ฉันเดาว่าคุณต้องหลีกหนีตัวละครที่มีความหมายเพื่อให้เป็นตัวอักษร
Jite

อย่างน้อยใน C ++ คุณสามารถใส่ typedef ได้ทุกที่ดังนั้นฉันไม่แน่ใจว่าทำไมคุณถึงบอกว่าต้องเป็น "ล่วงหน้า"
Vaughn Cato

คำตอบ:


109

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

FOO((std::map<int, int>), map_var);

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

เคล็ดลับที่ดีในการแก้ปัญหานี้คือใน C ++ คุณสามารถแยกชื่อประเภทจากชื่อประเภทในวงเล็บโดยใช้ประเภทฟังก์ชัน:

template<typename T> struct argument_type;
template<typename T, typename U> struct argument_type<T(U)> { typedef U type; };
#define FOO(t,name) argument_type<void(t)>::type name
FOO((std::map<int, int>), map_var);

เนื่องจากประเภทฟังก์ชันการสร้างจะไม่สนใจวงเล็บพิเศษคุณสามารถใช้มาโครนี้โดยมีหรือไม่มีวงเล็บโดยที่ชื่อประเภทไม่มีเครื่องหมายจุลภาค:

FOO((int), int_var);
FOO(int, int_var2);

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

#ifdef __cplusplus__
template<typename T> struct argument_type;
template<typename T, typename U> struct argument_type<T(U)> { typedef U type; };
#define FOO(t,name) argument_type<void(t)>::type name
#else
#define FOO(t,name) t name
#endif

นี่มันเจ๋งมาก. แต่คุณรู้ได้อย่างไรเกี่ยวกับเรื่องนี้? ฉันได้ลองใช้กลอุบายมากมายและไม่เคยคิดด้วยซ้ำว่าประเภทฟังก์ชันจะช่วยแก้ปัญหาได้
Will Custode

@WilliamCustode ตามที่ฉันจำได้ฉันกำลังศึกษาไวยากรณ์ของประเภทฟังก์ชันและการประกาศฟังก์ชันโดยอ้างอิงถึงปัญหาการแยกวิเคราะห์ที่ก่อกวนมากที่สุดดังนั้นฉันจึงทราบดีว่าวงเล็บซ้ำซ้อนสามารถนำไปใช้กับประเภทในบริบทนั้นได้
ecatmur

ฉันพบปัญหากับวิธีนี้เมื่อทำงานกับเทมเพลต สมมติว่ารหัสที่ฉันต้องการคือสิ่งนี้: template<class KeyType, class ValueType> void SomeFunc(FOO(std::map<KeyType, ValueType>) element) {}หากฉันใช้โซลูชันนี้ที่นี่โครงสร้างที่อยู่เบื้องหลังมาโครจะกลายเป็นชนิดที่ขึ้นอยู่กับประเภทและตอนนี้จำเป็นต้องใช้คำนำหน้าชื่อประเภท คุณสามารถเพิ่มได้ แต่การหักประเภทถูกทำลายดังนั้นตอนนี้คุณต้องแสดงรายการอาร์กิวเมนต์ประเภทด้วยตนเองเพื่อเรียกใช้ฟังก์ชัน ฉันใช้วิธีการของวัดในการกำหนดมาโครสำหรับลูกน้ำ อาจดูไม่สวยเท่าไหร่ แต่ก็ทำงานได้อย่างสมบูรณ์แบบ
Roger Sanders

ปัญหาเล็กน้อยเกี่ยวกับคำตอบ: ระบุว่าเครื่องหมายจุลภาคถูกละเว้นภายใน[] และ{}ไม่ได้ใช้งานได้กับ()เศร้าเท่านั้น ดู: อย่างไรก็ตามไม่มีข้อกำหนดสำหรับวงเล็บเหลี่ยมหรือวงเล็บเหลี่ยมเพื่อความสมดุล ...
VinGarcia

แต่น่าเสียดายที่นี้ไม่ได้ทำงานใน MSVC: godbolt.org/z/WPjYW8 ดูเหมือนว่า MSVC จะไม่อนุญาตให้เพิ่มหลาย parens และไม่สามารถแยกวิเคราะห์ได้ วิธีการแก้ปัญหาที่ไม่ได้เป็นสง่างาม แต่เร็วขึ้น (น้อย instantiations template) #define PROTECT(...) argument_type<void(__VA_ARGS__)>::typeคือการห่ออาร์กิวเมนต์จุลภาคเอ็ดเป็นแมโครเสื้อคลุม: ตอนนี้การส่งผ่านอาร์กิวเมนต์สามารถทำได้อย่างง่ายดายแม้จะผ่านมาโครหลายตัวและสำหรับประเภทง่ายๆคุณสามารถเลือกป้องกันได้ อย่างไรก็ตามประเภทของฟังก์ชันกลายเป็นตัวชี้ฟังก์ชันเมื่อประเมินเช่นนี้
Flamefire

119

หากคุณไม่สามารถใช้วงเล็บและคุณไม่ชอบโซลูชัน SINGLE_ARG ของ Mike ให้กำหนด COMMA:

#define COMMA ,

FOO(std::map<int COMMA int>, map_var);

นอกจากนี้ยังช่วยในกรณีที่คุณต้องการสตริงอาร์กิวเมนต์แมโครบางส่วนเช่นใน

#include <cstdio>
#include <map>
#include <typeinfo>

#define STRV(...) #__VA_ARGS__
#define COMMA ,
#define FOO(type, bar) bar(STRV(type) \
    " has typeid name \"%s\"", typeid(type).name())

int main()
{
    FOO(std::map<int COMMA int>, std::printf);
}

std::map<int , int> has typeid name "St3mapIiiSt4lessIiESaISt4pairIKiiEEE"ซึ่งพิมพ์


16
#define COMMA ว้าวคุณเพิ่งช่วยฉันได้ชั่วโมงทำงาน ... ทำไมฉันไม่คิดถึงปีที่แล้วนี้ ขอบคุณสำหรับการแบ่งปันความคิดนี้ สิ่งนี้ทำให้ฉันสามารถสร้างมาโครที่ตั้งค่าฟังก์ชันที่มีอาร์กิวเมนต์ต่างกันได้
moliad

28
บวก 1 เพื่อความสยอง
namezero

1
@kiw หากคุณ#define STRVX(...) STRV(__VA_ARGS__)และ#define STRV(...) # __VA_ARGS__จากนั้นstd::cout << STRV(type<A COMMA B>) << std::endl;จะพิมพ์type<A COMMA B>และจะพิมพ์std::cout << STRVX(type<A COMMA B>) << std::endl; type<A , B>( STRVมีไว้สำหรับ "varadic stringify" และSTRVXมีไว้สำหรับ "ขยายตัวแปร stringify")
not-a-user

1
@ ไม่ใช่ผู้ใช้ใช่ แต่ด้วยมาโครที่หลากหลายคุณไม่จำเป็นต้องใช้COMMAมาโครตั้งแต่แรก นั่นคือสิ่งที่ฉันลงเอยด้วย
kiw

ฉันไม่เคยใช้มัน แต่ +1 เพื่อความเฮฮา
Rafael Baptista

58

หากตัวประมวลผลล่วงหน้าของคุณรองรับมาโครตัวแปร:

#define SINGLE_ARG(...) __VA_ARGS__
#define FOO(type,name) type name

FOO(SINGLE_ARG(std::map<int, int>), map_var);

มิฉะนั้นจะน่าเบื่อกว่าเล็กน้อย:

#define SINGLE_ARG2(A,B) A,B
#define SINGLE_ARG3(A,B,C) A,B,C
// as many as you'll need

FOO(SINGLE_ARG2(std::map<int, int>), map_var);

โธ่เอ้ย ... ทำไม? ทำไมไม่ใส่แค่ในวงเล็บ?

15
@VladLazarenko: เพราะคุณไม่สามารถใส่โค้ดโดยพลการในวงเล็บได้ตลอดเวลา โดยเฉพาะอย่างยิ่งคุณไม่สามารถใส่วงเล็บรอบชื่อประเภทในตัวประกาศซึ่งเป็นสิ่งที่อาร์กิวเมนต์นี้จะกลายเป็น
Mike Seymour

2
... และเนื่องจากคุณอาจสามารถแก้ไขคำจำกัดความของมาโครได้เท่านั้นและไม่ใช่ทุกที่ที่เรียกมัน (ซึ่งอาจไม่อยู่ภายใต้การควบคุมของคุณหรืออาจแพร่กระจายไปทั่ว 1,000 ไฟล์เป็นต้น) สิ่งนี้เกิดขึ้นตัวอย่างเช่นเมื่อเพิ่มมาโครเพื่อรับช่วงหน้าที่จากฟังก์ชันที่มีชื่อเหมือนกัน
BeeOnRope

32

เพียงกำหนดFOOเป็น

#define UNPACK( ... ) __VA_ARGS__

#define FOO( type, name ) UNPACK type name

จากนั้นเรียกใช้ด้วยวงเล็บรอบอาร์กิวเมนต์ type เสมอเช่น

FOO( (std::map<int, int>), map_var );

แน่นอนว่าเป็นความคิดที่ดีที่จะยกตัวอย่างคำเรียกร้องในความคิดเห็นเกี่ยวกับนิยามมาโคร


ไม่แน่ใจว่าทำไมถึงเป็นเช่นนี้มันเป็นวิธีที่ดีกว่า Mike Seymours มาก มันง่ายและรวดเร็วและซ่อนจากผู้ใช้อย่างสมบูรณ์
iFreilicht

3
@iFreilicht: มีการโพสต์เพียงไม่กี่ปีต่อมา ;-)
ไชโยและ hth - Alf

5
และเพราะมันยากที่จะเข้าใจว่ามันทำงานอย่างไรและทำไม
VinGarcia

@ VinGarcia คุณสามารถอธิบายได้ว่าทำไม / มันทำงานอย่างไร? ทำไมต้องมีวงเล็บเมื่อเรียกมัน? สิ่งที่UNPACKต้องทำเมื่อมาใช้เช่นนี้) UNPACK type name? ทำไมถึงtypeได้รับประเภทอย่างถูกต้องเมื่อใช้งาน) UNPACK type name? เกิดอะไรขึ้นที่นี่?
ผู้ใช้

ไม่มี @user บางที Cheers และ hth สามารถตอบคุณได้
VinGarcia

4

มีอย่างน้อยสองวิธีในการดำเนินการนี้ ขั้นแรกคุณสามารถกำหนดมาโครที่รับหลายอาร์กิวเมนต์:

#define FOO2(type1, type2, name) type1, type2, name

หากคุณทำเช่นนั้นคุณอาจพบว่าคุณต้องกำหนดมาโครเพิ่มเติมเพื่อจัดการกับข้อโต้แย้งมากขึ้น

ประการที่สองคุณสามารถใส่วงเล็บรอบอาร์กิวเมนต์:

#define FOO(type, name) type name
F00((std::map<int, int>) map_var;

หากคุณทำเช่นนั้นคุณอาจพบว่าวงเล็บพิเศษทำให้ไวยากรณ์ของผลลัพธ์ผิดไป


สำหรับวิธีแก้ปัญหาแรกมาโครแต่ละตัวจะต้องมีชื่อที่แตกต่างกันเนื่องจากมาโครไม่โอเวอร์โหลด และอย่างที่สองถ้าคุณส่งชื่อประเภทมีโอกาสที่ดีมากที่จะใช้ประกาศตัวแปร (หรือ typedef) ดังนั้นวงเล็บจะทำให้เกิดปัญหา
James Kanze

4

เป็นไปได้ด้วยP99 :

#include "p99/p99.h"
#define FOO(...) P99_ALLBUTLAST(__VA_ARGS__) P99_LAST(__VA_ARGS__)
FOO()

โค้ดด้านบนจะตัดเฉพาะเครื่องหมายจุลภาคสุดท้ายในรายการอาร์กิวเมนต์เท่านั้น ตรวจสอบด้วยclang -E(P99 ต้องใช้คอมไพเลอร์ C99)


3

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


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