Khi nào dấu ngoặc đơn có tác dụng, ngoài việc ưu tiên toán tử?


91

Dấu ngoặc đơn trong C ++ được sử dụng ở nhiều nơi: ví dụ như trong các lệnh gọi hàm và nhóm biểu thức để ghi đè ưu tiên toán tử. Ngoài các dấu ngoặc đơn thừa bất hợp pháp (chẳng hạn như xung quanh danh sách đối số gọi hàm), một quy tắc chung - nhưng không phải là tuyệt đối của C ++ là các dấu ngoặc đơn phụ không bao giờ có hại :

5.1 Biểu thức chính [expr.prim]

5.1.1 Chung [expr.prim.general]

6 Một biểu thức trong ngoặc đơn là một biểu thức chính có kiểu và giá trị giống với kiểu và giá trị của biểu thức kèm theo. Sự hiện diện của dấu ngoặc đơn không ảnh hưởng đến việc biểu thức có phải là giá trị hay không. Biểu thức được đặt trong ngoặc đơn có thể được sử dụng trong các ngữ cảnh giống hệt như các ngữ cảnh mà biểu thức kèm theo có thể được sử dụng và với cùng một ý nghĩa, ngoại trừ trường hợp được chỉ định khác .

Câu hỏi : trong những ngữ cảnh nào dấu ngoặc đơn thay đổi ý nghĩa của chương trình C ++, ngoài việc ghi đè ưu tiên toán tử cơ bản?

LƯU Ý : Tôi coi việc hạn chế cú pháp con trỏ đến thành viên&qualified-id không có dấu ngoặc đơn nằm ngoài phạm vi vì nó hạn chế cú pháp hơn là cho phép hai cú pháp có ý nghĩa khác nhau. Tương tự, việc sử dụng dấu ngoặc đơn bên trong các định nghĩa macro bộ xử lý trước cũng bảo vệ chống lại sự ưu tiên của toán tử không mong muốn.


"Tôi coi độ phân giải & (đủ điều kiện-id) để trỏ đến thành viên là một ứng dụng ưu tiên toán tử." -- Tại sao vậy? Nếu bạn bỏ dấu ngoặc trong &(C::f), toán hạng của &vẫn C::flà, phải không?

@hvd expr.unary.op/4: Một con trỏ tới thành viên chỉ được hình thành khi một hàm rõ ràng &được sử dụng và toán hạng của nó là một id đủ điều kiện không được đặt trong dấu ngoặc đơn.
TemplateRex

Đúng, vậy điều đó có liên quan gì đến quyền ưu tiên của toán tử? (Nevermind, câu hỏi của bạn đã chỉnh sửa xóa mà lên.)

@hvd cập nhật, tôi đã nhầm lẫn RHS với LHS trong Q & A , và có các dấu ngoặc được sử dụng để ghi đè các ưu tiên của cuộc gọi chức năng ()trên bộ chọn con trỏ-to-thành viên::*
TemplateRex

1
Tôi nghĩ bạn nên chính xác hơn một chút về trường hợp nào cần xem xét. Ví dụ, dấu ngoặc đơn xung quanh tên kiểu để biến nó thành toán tử ép kiểu C (bất kể ngữ cảnh) hoàn toàn không tạo ra biểu thức có dấu ngoặc đơn. Mặt khác, về mặt kỹ thuật, tôi sẽ nói điều kiện sau if hoặc while là một biểu thức có dấu ngoặc đơn, nhưng vì dấu ngoặc đơn là một phần của cú pháp ở đây nên chúng không được xem xét. IMO cũng không nên bất kỳ trường hợp nào, trong đó không có dấu ngoặc đơn, biểu thức sẽ không còn được phân tích cú pháp như một đơn vị duy nhất, cho dù có liên quan đến quyền ưu tiên toán tử hay không.
Marc van Leeuwen

Câu trả lời:


112

TL; DR

Dấu ngoặc đơn thay đổi ý nghĩa của chương trình C ++ trong các ngữ cảnh sau:

  • ngăn chặn tra cứu tên phụ thuộc vào đối số
  • bật toán tử dấu phẩy trong ngữ cảnh danh sách
  • giải quyết sự mơ hồ của các phân đoạn khó chịu
  • suy ra tham chiếu trong decltypebiểu thức
  • ngăn chặn lỗi macro bộ xử lý trước

Ngăn tra cứu tên phụ thuộc vào đối số

Như được trình bày chi tiết trong Phụ lục A của Tiêu chuẩn, a post-fix expressioncó dạng (expression)là a primary expression, nhưng không phải id-expressionvà do đó không phải là unqualified-id. Điều này có nghĩa là tra cứu tên phụ thuộc đối số bị ngăn chặn trong các lệnh gọi hàm của biểu mẫu (fun)(arg)so với biểu mẫu thông thường fun(arg).

3.4.2 Tra cứu tên phụ thuộc vào đối số [basic.lookup.argdep]

1 Khi biểu thức hậu tố trong một lệnh gọi hàm (5.2.2) là một id không đủ tiêu chuẩn , các không gian tên khác không được xem xét trong quá trình tra cứu thông thường không đủ tiêu chuẩn (3.4.1) có thể được tìm kiếm và trong các không gian tên đó, hàm bạn của không gian tên phạm vi hoặc Có thể tìm thấy các khai báo mẫu hàm (11.3) không hiển thị. Những sửa đổi này đối với tìm kiếm phụ thuộc vào loại đối số (và đối với đối số mẫu mẫu, không gian tên của đối số mẫu). [ Thí dụ:

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
}

—Gửi ví dụ]

Bật toán tử dấu phẩy trong ngữ cảnh danh sách

Toán tử dấu phẩy có một ý nghĩa đặc biệt trong hầu hết các ngữ cảnh giống như danh sách (đối số hàm và mẫu, danh sách trình khởi tạo, v.v.). Dấu ngoặc đơn của biểu mẫu a, (b, c), dtrong các ngữ cảnh như vậy có thể cho phép toán tử dấu phẩy so với biểu mẫu thông thường a, b, c, dkhông áp dụng toán tử dấu phẩy.

5.18 Toán tử dấu phẩy [expr.comma]

2 Trong các ngữ cảnh mà dấu phẩy có ý nghĩa đặc biệt, [Ví dụ: trong danh sách các đối số cho các hàm (5.2.2) và danh sách các bộ khởi tạo (8.5) - ví dụ thêm] toán tử dấu phẩy như được mô tả trong Điều 5 chỉ có thể xuất hiện trong dấu ngoặc đơn. [ Thí dụ:

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

có ba đối số, đối số thứ hai có giá trị 5. —end ví dụ]

Giải quyết sự mơ hồ của các phân đoạn khó chịu

Khả năng tương thích ngược với C và cú pháp khai báo hàm phức tạp của nó có thể dẫn đến sự mơ hồ phân tích cú pháp đáng ngạc nhiên, được gọi là phân tích cú pháp khó chịu. Về cơ bản, bất kỳ thứ gì có thể được phân tích cú pháp như một khai báo sẽ được phân tích cú pháp thành một , mặc dù một phân tích cú pháp cạnh tranh cũng sẽ được áp dụng.

6.8 Giải quyết sự mơ hồ [stmt.ambig]

1 Có sự không rõ ràng trong ngữ pháp liên quan đến các câu lệnh và khai báo biểu thức: Một câu lệnh biểu thức có kiểu chuyển đổi kiểu hàm rõ ràng (5.2.3) là biểu thức con ngoài cùng bên trái của nó có thể không thể phân biệt được với một khai báo trong đó bộ khai báo đầu tiên bắt đầu bằng ( . Trong những trường hợp báo cáo kết quả là một bản tuyên bố .

8.2 Giải quyết sự mơ hồ [dcl.ambig.res]

1 Sự không rõ ràng phát sinh do sự giống nhau giữa một kiểu hàm và một khai báo được đề cập trong 6.8 cũng có thể xảy ra trong ngữ cảnh của một khai báo . Trong bối cảnh đó, sự lựa chọn là giữa một khai báo hàm với một tập hợp các dấu ngoặc đơn dự phòng xung quanh tên tham số và một khai báo đối tượng với một kiểu hàm kiểu làm bộ khởi tạo. Cũng như đối với những điểm không rõ ràng được đề cập trong 6.8, giải pháp là xem xét bất kỳ cấu trúc nào có thể là một khai báo . [Lưu ý: Một khai báo có thể được phân định rõ ràng bằng kiểu ép kiểu phi chức năng, bằng dấu = để chỉ ra sự khởi tạo hoặc bằng cách xóa dấu ngoặc đơn thừa xung quanh tên tham số. —Gửi ghi chú] [Ví dụ:

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
}

—Gửi ví dụ]

Một ví dụ nổi tiếng về vấn đề này là Most Vexing Parse , một cái tên được Scott Meyers phổ biến trong Mục 6 của cuốn sách STL Hiệu quả của ông :

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

Điều này khai báo một hàm data, có kiểu trả về là list<int>. Dữ liệu hàm nhận hai tham số:

  • Tham số đầu tiên được đặt tên dataFile. Đó là loại istream_iterator<int>. Các dấu ngoặc đơn xung quanh dataFilelà thừa và được bỏ qua.
  • Tham số thứ hai không có tên. Kiểu của nó là con trỏ đến hàm không lấy gì và trả về một istream_iterator<int>.

Đặt thêm dấu ngoặc đơn xung quanh đối số hàm đầu tiên (dấu ngoặc quanh đối số thứ hai là bất hợp pháp) sẽ giải quyết sự mơ hồ

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

C ++ 11 có cú pháp khởi tạo dấu ngoặc nhọn cho phép loại bỏ các vấn đề phân tích cú pháp như vậy trong nhiều ngữ cảnh.

Khấu trừ tham chiếu trong decltypebiểu thức

Ngược lại với autosuy luận kiểu, decltypecho phép suy ra tham chiếu (tham chiếu giá trị và giá trị). Các quy tắc phân biệt giữa decltype(e)decltype((e))biểu thức:

7.1.6.2 Bộ chỉ định loại đơn giản [dcl.type.simple]

4 Đối với một biểu thức e, kiểu được biểu thị bằngdecltype(e) được định nghĩa như sau:

- if elà một biểu thức id không có dấu ngoặc đơn hoặc quyền truy cập thành viên lớp không có dấu ngoặc đơn (5.2.5), decltype(e)là kiểu thực thể được đặt tên bởi e. Nếu không có thực thể như vậy, hoặc nếu eđặt tên cho một tập hợp các hàm quá tải, chương trình không được định hình;

- ngược lại, if elà một xvalue, decltype(e)is T&&, ở đâu Tlà loại e;

- nếu không, nếu elà giá trị, decltype(e)T&, ở đâu Tlà loại e;

- nếu không, decltype(e)là loại e.

Toán hạng của trình xác định kiểu khai báo là một toán hạng không được đánh giá (Điều 5). [ Thí dụ:

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&

—End example] [Lưu ý: Các quy tắc để xác định các loại liên quan decltype(auto)được quy định trong 7.1.6.4. —Gửi ghi chú]

Các quy tắc cho decltype(auto)có ý nghĩa tương tự đối với các dấu ngoặc đơn trong RHS của biểu thức khởi tạo. Đây là một ví dụ từ Câu hỏi thường gặp về C ++Câu hỏi & Đáp liên quan này

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

Trả về đầu tiên string, trả về thứ hai string &, là một tham chiếu đến biến cục bộ str.

Ngăn chặn các lỗi liên quan đến macro bộ xử lý trước

Có một loạt các điểm tinh vi với các macro tiền xử lý trong tương tác của chúng với ngôn ngữ C ++ thích hợp, trong đó phổ biến nhất được liệt kê dưới đây

  • sử dụng dấu ngoặc đơn xung quanh các tham số macro bên trong định nghĩa macro #define TIMES(A, B) (A) * (B);để tránh ưu tiên toán tử không mong muốn (ví dụ: trong TIMES(1 + 2, 2 + 1)đó kết quả là 9 nhưng sẽ mang lại 6 mà không có dấu ngoặc đơn xung quanh (A)(B)
  • sử dụng dấu ngoặc đơn xung quanh đối số macro có dấu phẩy bên trong: assert((std::is_same<int, int>::value));nếu không sẽ không biên dịch
  • sử dụng dấu ngoặc đơn xung quanh một hàm để bảo vệ khỏi việc mở rộng macro trong các tiêu đề được bao gồm: (min)(a, b)(với tác dụng phụ không mong muốn là vô hiệu hóa ADL)

7
Không thực sự thay đổi ý nghĩa chương trình, nhưng thực tiễn tốt nhất và ảnh hưởng đến cảnh báo do trình biên dịch phát ra: dấu ngoặc đơn bổ sung nên được sử dụng trong if/ whilenếu biểu thức là một phép gán. Ví dụ if (a = b)- cảnh báo (ý bạn là ==?), Trong khi if ((a = b))- không cảnh báo.
Csq

@Csq cảm ơn, bạn quan sát tốt, nhưng đó là cảnh báo của một trình biên dịch cụ thể và không được Tiêu chuẩn bắt buộc. Tôi không nghĩ rằng điều đó phù hợp với bản chất ngôn ngữ-luật sư của phần Hỏi & Đáp này.
TemplateRex

Có phải (min)(a, b)(với MACRO xấu min(A, B)) là một phần của phòng chống tra cứu tên phụ thuộc vào đối số không?
Jarod 42

@ Jarod42 tôi đoán như vậy, nhưng chúng ta hãy xem xét ví dụ macro ác và khác là nằm ngoài phạm vi của câu hỏi :-)
TemplateRex

5
@JamesKanze: Lưu ý rằng OP và TemplateRex là cùng một người ^ _ ^
Jarod42 Ngày

4

Nói chung, trong các ngôn ngữ lập trình, dấu ngoặc "phụ" ngụ ý rằng chúng không thay đổi thứ tự hoặc ý nghĩa phân tích cú pháp. Chúng đang được thêm vào để làm rõ thứ tự (ưu tiên của toán tử) vì lợi ích của mọi người đọc mã và tác dụng duy nhất của chúng sẽ là làm chậm quá trình biên dịch một chút và giảm lỗi của con người trong việc hiểu mã (có thể là tăng tốc quá trình phát triển tổng thể ).

Nếu một tập hợp các dấu ngoặc đơn thực sự thay đổi cách một biểu thức được phân tích cú pháp, thì theo định nghĩa, chúng không phải là bổ sung. Dấu ngoặc đơn biến một phân tích cú pháp bất hợp pháp / không hợp lệ thành một phân tích hợp pháp không phải là "bổ sung", mặc dù điều đó có thể chỉ ra một thiết kế ngôn ngữ kém.


2
chính xác, và đây cũng là quy tắc chung trong C ++ (xem phần trích dẫn Chuẩn trong câu hỏi), ngoại trừ trường hợp được chỉ ra khác . Mục đích của phần Hỏi và Đáp này là để chỉ ra những "điểm yếu" này.
TemplateRex
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.