วงเล็บเสริมจะมีผลเมื่อใดนอกเหนือจากลำดับความสำคัญของตัวดำเนินการ


91

วงเล็บใน C ++ ถูกใช้ในหลาย ๆ ที่เช่นในการเรียกใช้ฟังก์ชันและการจัดกลุ่มนิพจน์เพื่อแทนที่ลำดับความสำคัญของตัวดำเนินการ นอกเหนือจากวงเล็บเสริมที่ผิดกฎหมาย (เช่นรอบรายการอาร์กิวเมนต์การเรียกใช้ฟังก์ชัน) ทั่วไป - แต่ไม่ใช่กฎสัมบูรณ์ของ C ++ คือวงเล็บพิเศษจะไม่ทำร้าย :

5.1 นิพจน์หลัก [expr.prim]

5.1.1 ทั่วไป [expr.prim.general]

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

คำถาม : ในบริบทใดที่วงเล็บเสริมเปลี่ยนความหมายของโปรแกรม C ++ นอกเหนือจากการแทนที่ลำดับความสำคัญของตัวดำเนินการพื้นฐาน

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


"ฉันพิจารณาความละเอียด & (qualification-id) เพื่อชี้ถึงสมาชิกเพื่อเป็นแอปพลิเคชันที่มีลำดับความสำคัญของตัวดำเนินการ - ทำไมถึงเป็นอย่างนั้น? ถ้าคุณไม่ใส่วงเล็บใน&(C::f)ตัวถูกดำเนินการ&จะยังคงC::fอยู่ใช่หรือไม่

@hvd expr.unary.op/4: ตัวชี้ไปยังสมาชิกจะถูกสร้างขึ้นก็ต่อเมื่อมีการใช้ Explicit &และตัวถูกดำเนินการคือ ID ที่ผ่านการรับรองซึ่งไม่ได้อยู่ในวงเล็บ
TemplateRex

ถูกต้องแล้วสิ่งที่เกี่ยวข้องกับความสำคัญของตัวดำเนินการ? (ไม่เป็นไรคำถามที่คุณแก้ไขจะเคลียร์ได้)

@hvd อัปเดตแล้วฉันสับสน RHS กับ LHS ในคำถามและคำตอบนี้และที่นั่นมีการใช้ parens เพื่อแทนที่ลำดับความสำคัญของการเรียกใช้ฟังก์ชัน()ผ่านตัวเลือกตัวชี้ไปยังสมาชิก::*
TemplateRex

1
ฉันคิดว่าคุณควรมีความชัดเจนมากขึ้นเกี่ยวกับกรณีที่ต้องพิจารณา ตัวอย่างเช่นวงเล็บรอบ ๆ ชื่อประเภทเพื่อให้เป็นตัวดำเนินการแคสต์สไตล์ C (ไม่ว่าจะบริบทใดก็ตาม) อย่าสร้างนิพจน์ในวงเล็บเลย ในทางกลับกันฉันจะบอกว่าในทางเทคนิคเงื่อนไข after ifหรือwhileเป็นนิพจน์ในวงเล็บ แต่เนื่องจากวงเล็บเป็นส่วนหนึ่งของไวยากรณ์ที่นี่จึงไม่ควรพิจารณา ไม่ควรใช้ IMO ในกรณีใด ๆ โดยที่ไม่มีวงเล็บออกมานิพจน์จะไม่ถูกแยกวิเคราะห์เป็นหน่วยเดียวอีกต่อไปไม่ว่าจะเกี่ยวข้องกับลำดับความสำคัญของตัวดำเนินการหรือไม่ก็ตาม
Marc van Leeuwen

คำตอบ:


113

TL; ดร

วงเล็บเสริมจะเปลี่ยนความหมายของโปรแกรม C ++ ในบริบทต่อไปนี้:

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

การป้องกันการค้นหาชื่อที่ขึ้นกับอาร์กิวเมนต์

ในฐานะที่เป็นรายละเอียดในภาคผนวกของมาตรฐานเป็นpost-fix expressionของแบบฟอร์ม(expression)เป็นprimary expressionแต่ไม่ได้และดังนั้นจึงไม่ได้เป็นid-expression unqualified-idซึ่งหมายความว่าอาร์กิวเมนต์ขึ้นอยู่กับการค้นหาชื่อถูกขัดขวางในการเรียกฟังก์ชั่นในรูปแบบเมื่อเทียบกับรูปแบบเดิม(fun)(arg)fun(arg)

3.4.2 การค้นหาชื่อตามอาร์กิวเมนต์ [basic.lookup.argdep]

1 เมื่อนิพจน์ postfix ในการเรียกใช้ฟังก์ชัน (5.2.2) เป็นรหัสที่ไม่ถูกต้องอาจมีการค้นหาเนมสเปซอื่นที่ไม่ได้รับการพิจารณาในระหว่างการค้นหาที่ไม่มีเงื่อนไขตามปกติ (3.4.1) และในเนมสเปซเหล่านั้นฟังก์ชันเนมสเปซขอบเขตเพื่อนหรือ การประกาศเทมเพลตฟังก์ชัน (11.3) อาจมองไม่เห็นเป็นอย่างอื่น การปรับเปลี่ยนการค้นหาเหล่านี้ขึ้นอยู่กับประเภทของอาร์กิวเมนต์ (และสำหรับอาร์กิวเมนต์เทมเพลตเทมเพลตเนมสเปซของอาร์กิวเมนต์เทมเพลต) [ตัวอย่าง:

namespace N {
    struct S { };
    void f(S);
}

void g() {
    N::S s;
    f(s);   // OK: calls N::f
    (f)(s); // error: N::f not considered; parentheses
            // prevent argument-dependent lookup
}

- ส่งตัวอย่าง]

การเปิดใช้งานตัวดำเนินการลูกน้ำในบริบทรายการ

ตัวดำเนินการลูกน้ำมีความหมายพิเศษในบริบทที่เหมือนรายการส่วนใหญ่ (อาร์กิวเมนต์ของฟังก์ชันและเทมเพลตรายการตัวเริ่มต้น ฯลฯ ) วงเล็บของแบบฟอร์มa, (b, c), dในบริบทดังกล่าวสามารถเปิดใช้ตัวดำเนินการลูกน้ำเทียบกับรูปแบบปกติa, b, c, dที่ใช้ตัวดำเนินการลูกน้ำไม่ได้

5.18 ตัวดำเนินการจุลภาค [expr.comma]

2 ในบริบทที่ให้ลูกน้ำมีความหมายพิเศษ [ตัวอย่าง: ในรายการอาร์กิวเมนต์ของฟังก์ชัน (5.2.2) และรายการตัวเริ่มต้น (8.5) -ตัวอย่างตัวอย่าง] ตัวดำเนินการลูกน้ำตามที่อธิบายไว้ในข้อ 5 สามารถปรากฏได้เฉพาะในวงเล็บ [ตัวอย่าง:

f(a, (t=3, t+2), c);

มีอาร์กิวเมนต์สามตัวอันที่สองมีค่า 5 - ท้ายตัวอย่าง]

การแก้ปัญหาความไม่ชัดเจนของการแยกวิเคราะห์ที่รบกวน

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

6.8 การแก้ไขความคลุมเครือ [stmt.ambig]

1 มีความคลุมเครือในไวยากรณ์ที่เกี่ยวข้องกับนิพจน์ - คำสั่งและการประกาศ : นิพจน์ - คำสั่งที่มีการแปลงประเภท Explicit สไตล์ฟังก์ชัน (5.2.3) เนื่องจากนิพจน์ย่อยด้านซ้ายสุดสามารถแยกไม่ออกจากการประกาศโดยที่ตัวประกาศแรกเริ่มต้นด้วย ( . ในกรณีดังกล่าวเป็นคำสั่งประกาศ

8.2 การแก้ไขความคลุมเครือ [dcl.ambig.res]

1 ความคลุมเครือที่เกิดขึ้นจากความคล้ายคลึงกันระหว่างนักแสดงฟังก์ชั่นสไตล์และการประกาศที่กล่าวถึงใน 6.8 ยังสามารถเกิดขึ้นในบริบทของการประกาศที่ ในบริบทดังกล่าวทางเลือกอยู่ระหว่างการประกาศฟังก์ชันกับชุดวงเล็บที่ซ้ำซ้อนรอบชื่อพารามิเตอร์และการประกาศอ็อบเจ็กต์ที่มีการแคสต์ลักษณะฟังก์ชันเป็นตัวเริ่มต้น เช่นเดียวกับที่สำหรับงงงวยที่กล่าวถึงใน 6.8 ความละเอียดคือการพิจารณาสร้างใด ๆ ที่อาจจะประกาศประกาศ [หมายเหตุ: การประกาศสามารถแยกออกได้อย่างชัดเจนโดยการแคสต์แบบไม่ใช้งานฟังก์ชันโดย = เพื่อระบุการเริ่มต้นหรือโดยการลบวงเล็บที่ซ้ำซ้อนรอบชื่อพารามิเตอร์ —end note] [ตัวอย่าง:

struct S {
    S(int);
};

void foo(double a) {
    S w(int(a));  // function declaration
    S x(int());   // function declaration
    S y((int)a);  // object declaration
    S z = int(a); // object declaration
}

- ส่งตัวอย่าง]

ตัวอย่างที่มีชื่อเสียงของเรื่องนี้คือMost Vexing Parseซึ่งเป็นชื่อที่นิยมโดย Scott Meyers ในรายการที่ 6 ของหนังสือEffective STLของเขา:

ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // warning! this doesn't do
               istream_iterator<int>());        // what you think it does

นี้ประกาศฟังก์ชั่นที่มีผลตอบแทนประเภทคือdata list<int>ข้อมูลฟังก์ชันใช้สองพารามิเตอร์:

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

การวางวงเล็บเสริมรอบอาร์กิวเมนต์แรกของฟังก์ชัน (วงเล็บรอบอาร์กิวเมนต์ที่สองไม่ถูกต้อง) จะแก้ไขความคลุมเครือ

list<int> data((istream_iterator<int>(dataFile)), // note new parens
                istream_iterator<int>());          // around first argument
                                                  // to list's constructor

C ++ 11 มีไวยากรณ์ brace-initializer ที่ช่วยให้สามารถแบ่งขั้นตอนปัญหาการแยกวิเคราะห์ดังกล่าวในหลาย ๆ บริบท

การหักล้างการอ้างอิงในdecltypeนิพจน์

ตรงกันข้ามกับautoการหักประเภทdecltypeอนุญาตให้มีการอนุมานการอ้างอิง (การอ้างอิง lvalue และ rvalue) กฎแยกความแตกต่างระหว่างdecltype(e)และdecltype((e))นิพจน์:

7.1.6.2 ตัวระบุประเภทอย่างง่าย [dcl.type.simple]

4 เพราะการแสดงออกe, ประเภทแสดงโดยdecltype(e)มีการกำหนดดังต่อไปนี้:

- ถ้าeเป็น unparenthesized ID-การแสดงออกหรือการเข้าถึงสมาชิกระดับ unparenthesized (5.2.5) เป็นประเภทของกิจการตั้งชื่อโดยdecltype(e) eหากไม่มีเอนทิตีดังกล่าวหรือeตั้งชื่อชุดของฟังก์ชันที่โอเวอร์โหลดแสดงว่าโปรแกรมมีรูปแบบไม่ถูกต้อง

- มิฉะนั้นถ้าeเป็น Xvalue, decltype(e)เป็นT&&ที่Tเป็นประเภทของe;

- มิฉะนั้นถ้าeเป็น lvalue, decltype(e)เป็นT&ที่Tเป็นประเภทของe;

- มิฉะนั้นdecltype(e)เป็นประเภทของe.

ตัวถูกดำเนินการของตัวระบุประเภทปฏิเสธคือตัวถูกดำเนินการที่ไม่ได้ประเมิน (ข้อ 5) [ตัวอย่าง:

const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = 0;   // type is const int&&
decltype(i) x2;           // type is int
decltype(a->x) x3;        // type is double
decltype((a->x)) x4 = x3; // type is const double&

- ตัวอย่าง] [หมายเหตุ: กฎสำหรับการกำหนดประเภทที่เกี่ยวข้อง decltype(auto)ระบุไว้ใน 7.1.6.4 - ส่งหมายเหตุ]

กฎสำหรับdecltype(auto)มีความหมายคล้ายกันสำหรับวงเล็บเสริมใน RHS ของนิพจน์การเริ่มต้น นี่คือตัวอย่างจากคำถามที่พบบ่อยเกี่ยวกับC ++และคำถามและคำตอบที่เกี่ยวข้อง

decltype(auto) look_up_a_string_1() { auto str = lookup1(); return str; }  //A
decltype(auto) look_up_a_string_2() { auto str = lookup1(); return(str); } //B

ผลตอบแทนแรกstringผลตอบแทนที่สองซึ่งมีการอ้างอิงถึงตัวแปรท้องถิ่นstring &str

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

มีโฮสต์ของรายละเอียดปลีกย่อยที่มีมาโครตัวประมวลผลล่วงหน้าในการโต้ตอบกับภาษา C ++ ที่เหมาะสมซึ่งส่วนใหญ่จะแสดงอยู่ด้านล่าง

  • ใช้วงเล็บรอบพารามิเตอร์มาโครภายในนิยามมาโคร#define TIMES(A, B) (A) * (B);เพื่อหลีกเลี่ยงการมีลำดับความสำคัญของตัวดำเนินการที่ไม่ต้องการ (เช่นTIMES(1 + 2, 2 + 1)ให้ผล 9 แต่จะให้ผล 6 โดยไม่มีวงเล็บรอบ(A)และ(B)
  • ใช้วงเล็บรอบอาร์กิวเมนต์มาโครที่มีเครื่องหมายจุลภาคอยู่ภายในassert((std::is_same<int, int>::value));ซึ่งจะไม่สามารถรวบรวมได้
  • ใช้วงเล็บรอบฟังก์ชันเพื่อป้องกันการขยายมาโครในส่วนหัวที่รวมไว้: (min)(a, b)(ด้วยผลข้างเคียงที่ไม่ต้องการจากการปิดใช้งาน ADL ด้วย)

7
ไม่ได้เปลี่ยนความหมายของโปรแกรมอย่างแท้จริง แต่แนวทางปฏิบัติที่ดีที่สุดและมีผลต่อคำเตือนที่คอมไพเลอร์เปล่งออกมา: ควรใช้วงเล็บเสริมในif/ whileถ้านิพจน์เป็นการกำหนด เช่นif (a = b)- คำเตือน (คุณหมายถึง==?) ในขณะที่if ((a = b))- ไม่มีคำเตือน
Csq

@Csq ขอบคุณข้อสังเกตที่ดี แต่นั่นเป็นคำเตือนโดยคอมไพเลอร์เฉพาะและไม่ได้รับคำสั่งจาก Standard ฉันไม่คิดว่าสิ่งนี้จะเหมาะกับลักษณะทนายความด้านภาษาของคำถาม & คำตอบนี้
TemplateRex

ไม่(min)(a, b)(กับความชั่วร้าย MACRO min(A, B)) เป็นส่วนหนึ่งของการป้องกันการค้นหาชื่ออาร์กิวเมนต์ขึ้นอยู่กับ?
จรด 42

@ Jarod42 ฉันเดาอย่างนั้น แต่ลองพิจารณามาโครที่ชั่วร้ายเช่นนี้และอื่น ๆที่อยู่นอกขอบเขตของคำถาม :-)
TemplateRex

5
@JamesKanze: โปรดทราบว่า OP และ TemplateRex เป็นบุคคลเดียวกัน ^ _ ^
Jarod42

4

โดยทั่วไปในภาษาโปรแกรมวงเล็บ "พิเศษ" หมายความว่าพวกเขาไม่ได้เปลี่ยนลำดับหรือความหมายในการแยกวิเคราะห์ พวกเขากำลังถูกเพิ่มเพื่อชี้แจงลำดับ (ลำดับความสำคัญของตัวดำเนินการ) เพื่อประโยชน์ของผู้ที่อ่านโค้ดและผลกระทบเดียวของพวกเขาคือการทำให้กระบวนการคอมไพล์ช้าลงเล็กน้อยและลดข้อผิดพลาดของมนุษย์ในการทำความเข้าใจโค้ด (อาจทำให้กระบวนการพัฒนาโดยรวมเร็วขึ้น ).

หากชุดของวงเล็บเปลี่ยนวิธีการแยกวิเคราะห์นิพจน์จริง ๆก็จะเป็นไปตามคำจำกัดความไม่เกิน วงเล็บที่เปลี่ยนการแยกวิเคราะห์ที่ผิดกฎหมาย / ไม่ถูกต้องให้กลายเป็นสิ่งที่ถูกต้องตามกฎหมายนั้นไม่ "พิเศษ" แม้ว่าจะชี้ให้เห็นถึงการออกแบบภาษาที่ไม่ดีก็ตาม


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