Tại sao chúng ta yêu cầu yêu cầu?


161

Một trong những góc của khái niệm C ++ 20 là có những tình huống nhất định mà bạn phải viết requires requires. Chẳng hạn, ví dụ này từ [expr.prim.req] / 3 :

Một biểu thức đòi hỏi cũng có thể được sử dụng trong mệnh đề đòi hỏi ([temp]) như một cách viết các ràng buộc ad hoc trên các đối số mẫu như dưới đây:

template<typename T>
  requires requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

Yêu cầu đầu tiên giới thiệu mệnh đề đòi hỏi và lần thứ hai giới thiệu biểu thức đòi hỏi .

Lý do kỹ thuật đằng sau cần requirestừ khóa thứ hai là gì? Tại sao chúng ta không thể cho phép viết:

template<typename T>
  requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

(Lưu ý: vui lòng không trả lời ngữ pháp requiresđó)


25
Gợi ý: "Có bất cứ điều gì đòi hỏi phải yêu cầu không?". Nghiêm trọng hơn, tôi có linh cảm rằng đó là lý do tương tự đằng sau noexcept(noexcept(...)).
Quentin

11
Hai requirestừ này là từ đồng âm theo quan điểm của tôi: chúng trông giống nhau, đánh vần giống nhau, có mùi giống nhau, nhưng về bản chất là khác nhau. Nếu tôi đề nghị sửa chữa, tôi đề nghị đổi tên một trong số họ.
YSC

5
@YSC - co_requires? (Xin lỗi, không thể cưỡng lại).
Người kể chuyện - Unslander Monica

122
Sự điên rồ sẽ dừng lại ở đâu? Điều tiếp theo bạn biết, chúng tôi sẽ có long long.
Eljay

8
@StoryTeller: "Yêu cầu bắt buộc?" thậm chí còn được nhắc đến nhiều hơn nữa !!
PW

Câu trả lời:


81

Đó là bởi vì ngữ pháp đòi hỏi nó. Nó làm.

Một requireskhó khăn không phải sử dụng một requiresbiểu thức. Nó có thể sử dụng bất kỳ biểu thức hằng số boolean tùy ý nhiều hơn hoặc ít hơn. Do đó, requires (foo)phải là một requiresràng buộc chính đáng .

Một requires biểu thức (thứ đó kiểm tra xem một số thứ có tuân theo các ràng buộc nhất định hay không) là một cấu trúc riêng biệt; nó chỉ được giới thiệu bởi cùng một từ khóa. requires (foo f)sẽ là khởi đầu của một requiresbiểu thức hợp lệ .

Những gì bạn muốn là nếu bạn sử dụng requiresở một nơi chấp nhận các ràng buộc, bạn sẽ có thể thực hiện một "ràng buộc + biểu thức" ra khỏi requiresmệnh đề.

Vì vậy, đây là câu hỏi: nếu bạn đặt requires (foo)vào một nơi thích hợp cho một ràng buộc yêu cầu ... trình phân tích cú pháp phải đi bao xa trước khi có thể nhận ra rằng đây là một ràng buộc yêu cầu thay vì ràng buộc + biểu hiện theo cách bạn muốn được?

Xem xét điều này:

void bar() requires (foo)
{
  //stuff
}

Nếu foolà một kiểu, thì đó (foo)là một danh sách tham số của biểu thức yêu cầu và mọi thứ trong đó {}không phải là phần thân của hàm mà là phần thân của requiresbiểu thức đó . Mặt khác, foolà một biểu thức trong một requiresmệnh đề.

Chà, bạn có thể nói rằng trình biên dịch chỉ nên tìm ra cái gì foolà đầu tiên. Nhưng C ++ thực sự không thích điều đó khi hành động cơ bản phân tích chuỗi các mã thông báo yêu cầu trình biên dịch tìm hiểu ý nghĩa của các định danh đó trước khi nó có thể hiểu ý nghĩa của các mã thông báo. Có, C ++ nhạy cảm với bối cảnh, vì vậy điều này xảy ra. Nhưng ủy ban thích tránh nó khi có thể.

Vì vậy, có, đó là ngữ pháp.


2
Liệu nó có ý nghĩa để có một danh sách tham số với một loại nhưng không có tên?
NathanOliver

3
@Quentin: Chắc chắn có những trường hợp phụ thuộc vào ngữ cảnh trong ngữ pháp C ++. Nhưng ủy ban thực sự cố gắng giảm thiểu điều đó và họ chắc chắn không thích thêm nữa .
Nicol Bolas

3
@RobertAndrzejuk: Nếu requiresxuất hiện sau một <>tập hợp các đối số khuôn mẫu hoặc sau danh sách tham số hàm, thì đó là mệnh đề bắt buộc. Nếu requiresxuất hiện ở nơi một biểu thức hợp lệ, thì đó là biểu thức yêu cầu. Điều này có thể được xác định bởi cấu trúc của cây phân tích, chứ không phải nội dung của cây phân tích (chi tiết cụ thể về cách định danh được xác định sẽ là nội dung của cây).
Nicol Bolas

6
@RobertAndrzejuk: Chắc chắn, biểu thức yêu cầu có thể đã sử dụng một từ khóa khác. Nhưng các từ khóa có chi phí rất lớn trong C ++, vì chúng có khả năng phá vỡ bất kỳ chương trình nào sử dụng mã định danh đã trở thành một từ khóa. Đề xuất khái niệm đã giới thiệu hai từ khóa: conceptrequires. Giới thiệu một phần ba, khi thứ hai sẽ có thể bao gồm cả hai trường hợp không có vấn đề ngữ pháp và một số vấn đề phải đối mặt với người dùng, chỉ là lãng phí. Rốt cuộc, vấn đề trực quan duy nhất là từ khóa xảy ra được lặp lại hai lần.
Nicol Bolas

3
@RobertAndrzejuk dù sao đó cũng là một thực tiễn tồi đối với các ràng buộc nội tuyến như thế vì bạn không nhận được sự trợ cấp như thể bạn đã viết một khái niệm. Vì vậy, sử dụng một định danh cho việc sử dụng thấp như vậy không được đề xuất là một ý tưởng tốt.
Rakete1111

60

Tình hình hoàn toàn tương tự noexcept(noexcept(...)). Chắc chắn, điều này nghe giống như một điều xấu hơn là một điều tốt, nhưng hãy để tôi giải thích. :) Chúng ta sẽ bắt đầu với những gì bạn đã biết:

C ++ 11 có " noexcept-clauses" và " noexcept-expressions." Họ làm những việc khác nhau.

  • A noexcept-clins nói, "Chức năng này sẽ không được chấp nhận khi ... (một số điều kiện)." Nó tiếp tục khai báo hàm, lấy tham số boolean và gây ra thay đổi hành vi trong hàm khai báo.

  • Một noexcept-expression nói, "Trình biên dịch, xin vui lòng cho tôi biết liệu (một số biểu thức) là không có ngoại lệ." Nó là một biểu thức boolean. Nó không có "tác dụng phụ" đối với hoạt động của chương trình - nó chỉ yêu cầu trình biên dịch trả lời cho câu hỏi có / không. "Là biểu hiện này không có ngoại lệ?"

Chúng ta có thể lồng một noexceptbiểu hiện bên trong một noexceptphần, nhưng chúng ta thường coi đó là phong cách xấu để làm như vậy.

template<class T>
void incr(T t) noexcept(noexcept(++t));  // NOT SO HOT

Nó được coi là phong cách tốt hơn để gói gọn sự thể noexcepthiện trong một đặc điểm loại.

template<class T> inline constexpr bool is_nothrow_incrable_v =
    noexcept(++std::declval<T&>());  // BETTER, PART 1

template<class T>
void incr(T t) noexcept(is_nothrow_incrable_v<T>);  // BETTER, PART 2

Bản thảo làm việc của C ++ 2a có " requires-clauses" và "-expressions" requires. Họ làm những việc khác nhau.

  • A requires-clause nói, "Chức năng này sẽ tham gia giải quyết quá tải khi ... (một số điều kiện)." Nó tiếp tục khai báo hàm, lấy tham số boolean và gây ra thay đổi hành vi trong hàm khai báo.

  • Một requires-expression nói, "Trình biên dịch, xin vui lòng cho tôi biết liệu (một số bộ biểu thức) có được định dạng tốt hay không." Nó là một biểu thức boolean. Nó không có "tác dụng phụ" đối với hoạt động của chương trình - nó chỉ yêu cầu trình biên dịch trả lời cho câu hỏi có / không. "Biểu hiện này có được hình thành tốt không?"

Chúng ta có thể lồng một requiresbiểu hiện bên trong một requiresphần, nhưng chúng ta thường coi đó là phong cách xấu để làm như vậy.

template<class T>
void incr(T t) requires (requires(T t) { ++t; });  // NOT SO HOT

Nó được coi là phong cách tốt hơn để gói gọn sự thể requireshiện trong một đặc điểm ...

template<class T> inline constexpr bool is_incrable_v =
    requires(T t) { ++t; };  // BETTER, PART 1

template<class T>
void incr(T t) requires is_incrable_v<T>;  // BETTER, PART 2

... hoặc trong một khái niệm (C ++ 2a Work Draft).

template<class T> concept Incrable =
    requires(T t) { ++t; };  // BETTER, PART 1

template<class T>
void incr(T t) requires Incrable<T>;  // BETTER, PART 2

1
Tôi không thực sự mua lập luận này. noexceptcó vấn đề mà noexcept(f())có thể có nghĩa hoặc giải thích f()như một boolean mà chúng ta đang sử dụng để thiết lập các đặc điểm kỹ thuật hoặc kiểm tra hay không f()noexcept. requireskhông có sự mơ hồ này bởi vì các biểu thức kiểm tra tính hợp lệ của nó đã được giới thiệu với {}s. Sau đó, đối số về cơ bản là "ngữ pháp nói như vậy."
Barry

@Barry: Xem bình luận này . Nó xuất hiện {}là tùy chọn.
Eric

1
@Eric {}Không phải là tùy chọn, đó không phải là những gì bình luận cho thấy. Tuy nhiên, đó là một nhận xét tuyệt vời thể hiện sự mơ hồ phân tích. Có lẽ sẽ chấp nhận nhận xét đó (với một số giải thích) như một câu trả lời độc lập
Barry

1
requires is_nothrow_incrable_v<T>;nên làrequires is_incrable_v<T>;
Ruslan

16

Tôi nghĩ rằng trang khái niệm của cppreference giải thích điều này. Tôi có thể giải thích bằng "toán học" để nói, tại sao điều này phải như thế này:

Nếu bạn muốn xác định một khái niệm, bạn làm điều này:

template<typename T>
concept Addable = requires (T x) { x + x; }; // requires-expression

Nếu bạn muốn khai báo một hàm sử dụng khái niệm đó, bạn làm điều này:

template<typename T> requires Addable<T> // requires-clause, not requires-expression
T add(T a, T b) { return a + b; }

Bây giờ nếu bạn không muốn định nghĩa khái niệm một cách riêng biệt, tôi đoán tất cả những gì bạn phải làm là một số thay thế. Lấy phần này requires (T x) { x + x; };và thay thế Addable<T>phần đó, và bạn sẽ nhận được:

template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }

đó là những gì bạn đang hỏi về.


4
Tôi không nghĩ là những gì câu hỏi đang yêu cầu. Điều này là giải thích ngữ pháp, nhiều hay ít.
qua đường vào

Nhưng tại sao bạn không thể có template<typename T> requires (T x) { x + x; }và yêu cầu yêu cầu đó có thể là cả mệnh đề và biểu thức?
NathanOliver

2
@NathanOliver: Bởi vì bạn đang buộc trình biên dịch diễn giải một cấu trúc này thành một cấu trúc khác. Một requireskhoản -Như-chế không phải là một requires-expression. Đó chỉ là một cách sử dụng có thể của nó.
Nicol Bolas

2
@TheQuantumPhysicist Điều tôi nhận được với nhận xét của mình là câu trả lời này chỉ giải thích cú pháp. Không phải những gì lý do kỹ thuật thực tế chúng ta phải requires requires. Họ có thể đã thêm một cái gì đó vào ngữ pháp để cho phép template<typename T> requires (T x) { x + x; }nhưng họ đã không làm thế. Barry muốn biết lý do tại sao họ đã không
NathanOliver

3
Nếu chúng ta thực sự chơi trò tìm kiếm ngữ pháp ở đây, OK, tôi sẽ cắn. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; có nghĩa là một cái gì đó khác biệt nếu bạn loại bỏ một trong những requireses.
Quuxplusone

12

Tôi đã tìm thấy một nhận xét từ Andrew Sutton (một trong những tác giả của khái niệm, người đã triển khai nó trong gcc) khá hữu ích trong vấn đề này, vì vậy tôi nghĩ rằng tôi chỉ trích dẫn nó ở đây gần như toàn bộ:

Cách đây không lâu, các biểu thức yêu cầu (cụm từ được giới thiệu bởi yêu cầu thứ hai) không được phép trong các biểu thức ràng buộc (cụm từ được giới thiệu bởi yêu cầu đầu tiên). Nó chỉ có thể xuất hiện trong các định nghĩa khái niệm. Trên thực tế, đây chính xác là những gì được đề xuất trong phần của bài báo đó, nơi yêu cầu đó xuất hiện.

Tuy nhiên, vào năm 2016, đã có một đề xuất để giảm bớt hạn chế đó [Ghi chú của biên tập viên: P0266 ]. Lưu ý gạch ngang của đoạn 4 trong phần 4 của bài báo. Và do đó sinh ra đòi hỏi phải có.

Nói thật, tôi chưa bao giờ thực sự thực hiện hạn chế đó trong GCC, vì vậy nó luôn luôn có thể. Tôi nghĩ rằng Walter có thể đã phát hiện ra điều đó và thấy nó hữu ích, dẫn đến bài báo đó.

Bất cứ ai nghĩ rằng tôi không nhạy cảm với việc viết lách đòi hỏi hai lần, tôi đã dành một chút thời gian để xác định xem điều đó có thể được đơn giản hóa hay không. Câu trả lời ngắn gọn: không.

Vấn đề là có hai cấu trúc ngữ pháp cần được giới thiệu sau một danh sách tham số mẫu: rất phổ biến là một biểu thức ràng buộc (như P && Q) và đôi khi yêu cầu cú pháp (như requires (T a) { ... }). Đó gọi là biểu thức yêu cầu.

Việc đầu tiên yêu cầu giới thiệu các ràng buộc. Thứ hai yêu cầu giới thiệu biểu thức yêu cầu. Đó chỉ là cách ngữ pháp sáng tác. Tôi không thấy khó hiểu chút nào.

Tôi đã cố gắng, tại một thời điểm, để thu gọn những thứ này thành một yêu cầu duy nhất. Thật không may, điều đó dẫn đến một số vấn đề phân tích cú pháp khó khăn nghiêm trọng. Bạn không thể dễ dàng biết được, ví dụ nếu a (sau yêu cầu biểu thị một biểu thức con lồng nhau hoặc danh sách tham số. Tôi không tin rằng có một sự phân định hoàn hảo các cú pháp đó (xem phần cơ sở cho cú pháp khởi tạo thống nhất; vấn đề này cũng có).

Vì vậy, bạn đưa ra lựa chọn: thực hiện yêu cầu giới thiệu một biểu thức (như hiện tại) hoặc làm cho nó giới thiệu một danh sách các yêu cầu được tham số hóa.

Tôi đã chọn cách tiếp cận hiện tại bởi vì hầu hết thời gian (như trong gần 100% thời gian), tôi muốn một cái gì đó ngoài một biểu thức yêu cầu. Và trong trường hợp cực kỳ hiếm hoi tôi đã muốn có một biểu thức đòi hỏi cho các ràng buộc ad hoc, tôi thực sự không ngại viết từ đó hai lần. Đó là một chỉ báo rõ ràng rằng tôi đã không phát triển một sự trừu tượng âm thanh đủ cho mẫu. (Bởi vì nếu tôi có, nó sẽ có một cái tên.)

Tôi có thể đã chọn để làm cho các yêu cầu giới thiệu một biểu thức yêu cầu. Điều đó thực sự tồi tệ hơn, vì thực tế tất cả các ràng buộc của bạn sẽ bắt đầu giống như thế này:

template<typename T>
  requires { requires Eq<T>; }
void f(T a, T b);

Ở đây, yêu cầu thứ 2 được gọi là yêu cầu lồng nhau; nó đánh giá biểu thức của nó (mã khác trong khối biểu thức yêu cầu không được ước tính). Tôi nghĩ rằng đây là cách tồi tệ hơn hiện trạng. Bây giờ, bạn có thể viết yêu cầu hai lần ở khắp mọi nơi.

Tôi cũng có thể đã sử dụng nhiều từ khóa hơn. Đây là một vấn đề theo đúng nghĩa của nó --- và nó không chỉ là đổ xe đạp. Có thể có một cách để "phân phối lại" các từ khóa để tránh trùng lặp, nhưng tôi đã không suy nghĩ nghiêm túc. Nhưng điều đó không thực sự thay đổi bản chất của vấn đề.


-10

Bởi vì bạn đang nói rằng một điều A có yêu cầu B và yêu cầu B có yêu cầu C.

Điều A yêu cầu B mà lần lượt yêu cầu C.

Mệnh đề "đòi hỏi" tự nó đòi hỏi một cái gì đó.

Bạn có điều A (yêu cầu B (yêu cầu C)).

Meh. :)


4
Nhưng theo các câu trả lời khác, thứ nhất và thứ hai requireskhông giống nhau về mặt khái niệm (một là mệnh đề, một biểu thức). Trong thực tế, nếu tôi hiểu chính xác, hai bộ ()trong requires (requires (T x) { x + x; })có ý nghĩa rất khác nhau (bên ngoài là tùy chọn và luôn chứa một biểu thức boolean; bên trong là một phần bắt buộc của việc đưa ra biểu thức yêu cầu và không cho phép biểu thức thực tế).
Max Langhof

2
@MaxLanghof Bạn đang nói rằng các yêu cầu khác nhau? : D
Các cuộc đua nhẹ nhàng trong quỹ đạo
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.