Có thực sự có một lý do tại sao quá tải && và || không bị đoản mạch?


137

Hành vi ngắn mạch của các nhà khai thác &&||là một công cụ tuyệt vời cho các lập trình viên.

Nhưng tại sao họ mất hành vi này khi quá tải? Tôi hiểu rằng các toán tử chỉ là đường cú pháp cho các hàm nhưng các toán tử boolcó hành vi này, tại sao nó phải bị hạn chế đối với loại đơn này? Có bất kỳ lý do kỹ thuật đằng sau này?


1
@PiotrS. Câu hỏi đó có lẽ là câu trả lời. Tôi đoán tiêu chuẩn có thể định nghĩa một cú pháp mới chỉ cho mục đích này. Có lẽ giống như operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht

1
@PiotrS.: Hãy xem xét logic ba trạng thái : {true, false, nil}. Vì nil&& x == nilnó có thể ngắn mạch.
MSalters

1
@MSalters: Hãy xem xét std::valarray<bool> a, b, c;, làm thế nào bạn tưởng tượng a || b || cđược ngắn mạch?
Piotr Skotnicki

4
@PiotrS.: Tôi lập luận rằng tồn tại ít nhất một loại không phải là bool cho ngắn mạch có ý nghĩa. Tôi không tranh luận rằng shortcircuiting có ý nghĩa cho mọi loại không bool.
MSalters

3
Không ai đề cập đến điều này, nhưng cũng có vấn đề về khả năng tương thích ngược. Trừ khi được chăm sóc đặc biệt để hạn chế các trường hợp áp dụng ngắn mạch này, việc ngắn mạch như vậy có thể phá vỡ mã hiện có gây quá tải operator&&hoặc operator||phụ thuộc vào cả hai toán hạng được đánh giá. Duy trì khả năng tương thích ngược là (hoặc nên) quan trọng khi thêm các tính năng vào ngôn ngữ hiện có.
David Hammen

Câu trả lời:


151

Tất cả các quy trình thiết kế dẫn đến sự thỏa hiệp giữa các mục tiêu không tương thích lẫn nhau. Thật không may, quá trình thiết kế cho &&toán tử quá tải trong C ++ đã tạo ra một kết quả cuối cùng khó hiểu: rằng chính tính năng bạn muốn &&- hành vi ngắn mạch của nó - bị bỏ qua.

Các chi tiết về cách quá trình thiết kế đó kết thúc ở nơi không may này, những người tôi không biết. Tuy nhiên, có liên quan để xem làm thế nào một quá trình thiết kế sau này đã tính đến kết quả khó chịu này. Trong C #, &&toán tử quá tải ngắn mạch. Làm thế nào mà các nhà thiết kế của C # đạt được điều đó?

Một trong những câu trả lời khác cho thấy "nâng lambda". Đó là:

A && B

có thể được nhận ra như một cái gì đó tương đương về mặt đạo đức với:

operator_&& ( A, ()=> B )

trong đó đối số thứ hai sử dụng một số cơ chế để đánh giá lười biếng để khi được đánh giá, các tác dụng phụ và giá trị của biểu thức được tạo ra. Việc thực hiện toán tử quá tải sẽ chỉ thực hiện đánh giá lười biếng khi cần thiết.

Đây không phải là những gì nhóm thiết kế C # đã làm. (Ngoài ra: mặc dù việc nâng lambda những gì tôi đã làm khi đến lúc phải thực hiện biểu diễn cây biểu thức của ??toán tử, đòi hỏi một số thao tác chuyển đổi phải được thực hiện một cách lười biếng. Mô tả chi tiết tuy nhiên sẽ là một sự cải tiến lớn. hoạt động nhưng đủ nặng mà chúng tôi muốn tránh nó.)

Thay vào đó, giải pháp C # chia vấn đề thành hai vấn đề riêng biệt:

  • chúng ta nên đánh giá toán hạng bên tay phải?
  • Nếu câu trả lời ở trên là "có", thì làm thế nào để chúng ta kết hợp hai toán hạng?

Do đó, vấn đề được giải quyết bằng cách làm cho nó quá tải &&trực tiếp. Thay vào đó, trong C #, bạn phải quá tải hai toán tử, mỗi toán tử trả lời một trong hai câu hỏi đó.

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...

(Ngoài ra: trên thực tế, ba. C # yêu cầu rằng nếu toán tử falseđược cung cấp thì toán tử truecũng phải được cung cấp, câu trả lời cho câu hỏi: đây có phải là "true-ish?". Thông thường sẽ không có lý do gì để chỉ cung cấp một toán tử như vậy nên C # yêu cầu cả hai.)

Xem xét một tuyên bố của mẫu:

C cresult = cleft && cright;

Trình biên dịch tạo mã cho điều này như bạn nghĩ rằng bạn đã viết giả này C #:

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

Như bạn có thể thấy, phía bên tay trái luôn được đánh giá. Nếu nó được xác định là "false-ish" thì đó là kết quả. Mặt khác, phía bên tay phải được ước tính và toán tử do người dùng định nghĩa háo hức& được gọi.

Các ||nhà điều hành được xác định theo cách tương tự, như một lời khẩn cầu của nhà điều hành trung thực và sự háo hức |điều hành:

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

Bằng việc xác định tất cả bốn nhà khai thác - true, false, &|- C # cho phép bạn không chỉ nói cleft && crightmà còn không chập mạch cleft & cright, và cũng có thể if (cleft) if (cright) ..., và c ? consequence : alternativewhile(c), và vân vân.

Bây giờ, tôi đã nói rằng tất cả các quy trình thiết kế là kết quả của sự thỏa hiệp. Ở đây, các nhà thiết kế ngôn ngữ C # đã quản lý để có được ngắn mạch &&||đúng, nhưng làm như vậy đòi hỏi quá tải bốn toán tử thay vì hai , điều mà một số người cảm thấy khó hiểu. Tính năng đúng / sai của toán tử là một trong những tính năng ít được hiểu nhất trong C #. Mục tiêu có một ngôn ngữ hợp lý và đơn giản, quen thuộc với người dùng C ++ đã bị phản đối bởi mong muốn được đoản mạch và mong muốn không thực hiện nâng lambda hoặc các hình thức đánh giá lười biếng khác. Tôi nghĩ rằng đó là một vị trí thỏa hiệp hợp lý, nhưng điều quan trọng là phải nhận ra rằng đó một vị trí thỏa hiệp. Chỉ là khác thỏa hiệp vị trí hơn các nhà thiết kế của C ++ hạ cánh.

Nếu chủ đề thiết kế ngôn ngữ cho các toán tử đó làm bạn quan tâm, hãy xem xét việc đọc loạt bài của tôi về lý do tại sao C # không định nghĩa các toán tử này trên Booleans nullable:

http://ericlippert.com/2012/03/26/null-is-not-false-part-one/


1
@Ded repeatator: Bạn cũng có thể thích đọc câu hỏi và câu trả lời này: stackoverflow.com/questions/5965968/
Eric Lippert

5
Trong trường hợp này, tôi nghĩ rằng sự thỏa hiệp là nhiều hơn hợp lý. Những thứ phức tạp là thứ mà chỉ kiến ​​trúc sư của một thư viện lớp phải quan tâm, và đổi lại sự phức tạp này, nó làm cho việc tiêu thụ thư viện dễ dàng và trực quan hơn.
Cody Grey

1
@EricLippert Tôi tin rằng Envision đã nói rằng anh ấy đã xem bài đăng này và nghĩ rằng đó là bạn ... sau đó thấy anh ấy đã đúng. Anh không nói your postlà không liên quan. His noticing your distinct writing stylekhông liên quan.
WernerCD

5
Nhóm Microsoft không có đủ tín dụng cho (1) thực hiện một nỗ lực tốt được đánh dấu để thực hiện đúng trong C # và (2) nhận được nhiều lần hơn là không.
codenheim

2
@Voo: Nếu bạn chọn thực hiện chuyển đổi ngầm định boolthì bạn có thể sử dụng &&||không cần thực hiện operator true/falsehoặc operator &/|trong C # không có vấn đề gì. Vấn đề phát sinh chính xác trong tình huống không có chuyển đổi thành boolcó thể , hoặc nơi không mong muốn.
Eric Lippert

43

Vấn đề là (trong giới hạn của C ++ 98) toán hạng bên phải sẽ được chuyển đến hàm toán tử quá tải làm đối số. Làm như vậy, nó sẽ được đánh giá . Không có gì operator||()hoặc operator&&()mã có thể hoặc không thể làm điều đó sẽ tránh điều này.

Toán tử gốc là khác nhau, vì nó không phải là một hàm, nhưng được triển khai ở mức độ thấp hơn của ngôn ngữ.

Các tính năng ngôn ngữ bổ sung có thể đã làm cho việc đánh giá toán hạng bên tay phải có thể về mặt cú pháp . Tuy nhiên, họ không bận tâm vì chỉ có một vài trường hợp được lựa chọn trong đó điều này sẽ hữu ích về mặt ngữ nghĩa . (Giống như ? :, không có sẵn cho quá tải.

(Phải mất 16 năm để đưa lambdas trở thành tiêu chuẩn ...)

Đối với việc sử dụng ngữ nghĩa, xem xét:

objectA && objectB

Điều này sôi xuống:

template< typename T >
ClassA.operator&&( T const & objectB )

Hãy suy nghĩ về chính xác những gì bạn muốn làm với objectB (loại không xác định) ở đây, ngoài việc gọi một toán tử chuyển đổi boolvà cách bạn đặt nó thành các từ cho định nghĩa ngôn ngữ.

nếu bạn đang gọi chuyển đổi sang bool, thì ...

objectA && obectB

làm điều tương tự, bây giờ không? Vậy tại sao quá tải ở nơi đầu tiên?


7
Vâng, lỗi logic của bạn là lý do trong ngôn ngữ hiện được xác định về tác động của ngôn ngữ được xác định khác. ngày xưa rất nhiều người mới sử dụng để làm điều đó. "nhà xây dựng ảo". phải mất một lượng giải thích không phù hợp để đưa họ ra khỏi suy nghĩ hộp như vậy. dù sao đi nữa, với sự ngắn mạch của các toán tử tích hợp, có sự đảm bảo về việc không đánh giá đối số. đảm bảo như vậy cũng sẽ có mặt cho quá tải do người dùng định nghĩa, nếu ngắn mạch được xác định cho chúng.
Chúc mừng và hth. - Alf

1
@iFreilicht: Về cơ bản tôi đã nói điều tương tự như Ded repeatator hoặc Piotr, chỉ với các từ khác nhau. Tôi đã xây dựng một chút về điểm trong câu trả lời được chỉnh sửa. Cách này thuận tiện hơn nhiều, các phần mở rộng ngôn ngữ cần thiết (ví dụ lambdas) đã không tồn tại cho đến gần đây và dù sao lợi ích cũng không đáng kể. Một vài lần mà những người có trách nhiệm sẽ "thích" thứ gì đó chưa được thực hiện bởi các nhà xây dựng trình biên dịch, vào năm 1998, nó đã phản tác dụng. (Xem export.)
DevSolar

9
@iFreilicht: Một booltoán tử chuyển đổi cho cả hai lớp cũng có quyền truy cập vào tất cả các biến thành viên và hoạt động tốt với toán tử dựng sẵn. Bất cứ điều gì khác ngoài chuyển đổi sang bool không có ý nghĩa ngữ nghĩa cho việc đánh giá ngắn mạch! Cố gắng tiếp cận điều này từ quan điểm ngữ nghĩa, không phải là cú pháp: Bạn sẽ cố gắng đạt được điều gì, chứ không phải bạn sẽ làm thế nào về nó.
DevSolar

1
Tôi phải thừa nhận rằng tôi không thể nghĩ về một. Lý do duy nhất tồn tại ngắn mạch là vì nó tiết kiệm thời gian cho các hoạt động trên Booleans và bạn có thể biết kết quả của một biểu thức trước khi tất cả các đối số được đánh giá. Với các hoạt động AND khác, đó không phải là trường hợp và đó là lý do tại sao &&&không phải là cùng một toán tử. Cảm ơn đã giúp tôi nhận ra điều đó.
iFreilicht

8
@iFreilicht: Thay vào đó, mục đích của đoản mạch là do tính toán của phía bên tay trái có thể thiết lập sự thật của một điều kiện tiên quyết của phía bên tay phải . if (x != NULL && x->foo)đòi hỏi ngắn mạch, không phải vì tốc độ, mà là an toàn.
Eric Lippert

26

Một tính năng phải được nghĩ đến, thiết kế, thực hiện, ghi lại và vận chuyển.

Bây giờ chúng tôi nghĩ về nó, hãy xem tại sao bây giờ có thể dễ dàng (và khó thực hiện sau đó). Ngoài ra, hãy nhớ rằng chỉ có một lượng tài nguyên hạn chế, vì vậy việc thêm nó có thể đã cắt nhỏ thứ khác (bạn muốn báo trước điều gì cho nó?).


Về lý thuyết, tất cả các nhà khai thác có thể cho phép hành vi ngắn mạch chỉ với một tính năng ngôn ngữ bổ sung "nhỏ" , kể từ C ++ 11 (khi lambdas được giới thiệu, 32 năm sau khi "C với các lớp" bắt đầu vào năm 1979, vẫn còn 16 đáng kính sau c ++ 98):

C ++ sẽ chỉ cần một cách để chú thích một đối số là lười đánh giá - một lambda ẩn - để tránh việc đánh giá cho đến khi cần thiết và được cho phép (điều kiện trước đáp ứng).


Tính năng lý thuyết đó sẽ trông như thế nào (Hãy nhớ rằng bất kỳ tính năng mới nào sẽ được sử dụng rộng rãi)?

Một chú thích lazy, được áp dụng cho một đối số hàm làm cho hàm trở thành một khuôn mẫu mong đợi một functor và làm cho trình biên dịch đóng gói biểu thức vào một functor:

A operator&&(B b, __lazy C c) {return c;}

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);

Nó sẽ nhìn dưới bìa như:

template<class Func> A operator&&(B b, Func& f) {auto&& c = f(); return c;}
// With `f` restricted to no-argument functors returning a `C`.

// And the call:
operator&&(exp_b, [&]{return exp_c;});

Lưu ý đặc biệt rằng lambda vẫn bị ẩn và sẽ được gọi nhiều nhất một lần.
Không nên có sự suy giảm hiệu suất do điều này, ngoài việc giảm cơ hội loại bỏ phổ biến phụ.


Bên cạnh độ phức tạp triển khai và độ phức tạp về khái niệm (mọi tính năng đều tăng cả hai, trừ khi nó đủ làm giảm bớt sự phức tạp đó cho một số tính năng khác), chúng ta hãy xem xét một cân nhắc quan trọng khác: Khả năng tương thích ngược.

Mặc dù tính năng ngôn ngữ này sẽ không phá vỡ bất kỳ mã nào, nhưng nó sẽ thay đổi một cách tinh tế bất kỳ API nào lợi dụng nó, điều đó có nghĩa là bất kỳ việc sử dụng nào trong các thư viện hiện tại sẽ là một thay đổi đột ngột.

BTW: Tính năng này, trong khi dễ sử dụng hơn, mạnh hơn nhiều so với giải pháp chia tách C # &&||thành hai chức năng cho mỗi định nghĩa riêng biệt.


6
@iFreilicht: Bất kỳ câu hỏi nào của mẫu "tại sao tính năng X không tồn tại?" có cùng một câu trả lời: để tồn tại tính năng phải được nghĩ đến, được coi là một ý tưởng tốt, được thiết kế, chỉ định, thực hiện, thử nghiệm, ghi lại và chuyển đến người dùng cuối. Nếu bất kỳ một trong những điều đó đã không xảy ra, không có tính năng. Một trong những điều đó đã không xảy ra với tính năng được đề xuất của bạn; tìm ra cái nào là một vấn đề nghiên cứu lịch sử; bắt đầu nói chuyện với mọi người trong ủy ban thiết kế nếu bạn quan tâm một trong những điều đó chưa bao giờ được thực hiện.
Eric Lippert

1
@EricLippert: Và, tùy thuộc vào lý do nào, hãy lặp lại cho đến khi nó được thực hiện: Có lẽ nó được cho là quá phức tạp và không ai nghĩ sẽ đánh giá lại. Hoặc việc đánh giá lại kết thúc với những lý do khác nhau để từ chối so với tổ chức trước đó. (btw: Đã thêm ý chính cho nhận xét của bạn)
Ded repeatator

@Ded repeatator Với các mẫu biểu thức, không yêu cầu từ khóa lười biếng hay lambdas.
Sumant

Về mặt lịch sử, lưu ý rằng ngôn ngữ Algol 68 ban đầu có sự ép buộc "thủ tục" (cũng như giải mã, có nghĩa là gọi một hàm không tham số khi bối cảnh yêu cầu loại kết quả thay vì loại hàm). Điều này có nghĩa là một biểu thức của loại T ở vị trí yêu cầu giá trị của loại "hàm không tham số trả về T" (đánh vần là " Proc T" trong Algol 68) sẽ được chuyển đổi thành thân hàm trả về biểu thức đã cho (ẩn lambda). Tính năng này đã bị xóa (không giống như giải mã) trong bản sửa đổi ngôn ngữ năm 1973.
Marc van Leeuwen

... Đối với C ++, một cách tiếp cận tương tự có thể là khai báo các toán tử muốn &&lấy một đối số kiểu "con trỏ để hàm trả về T" và một quy tắc chuyển đổi bổ sung cho phép biểu thức đối số của loại T được chuyển đổi thành biểu thức lambda. Lưu ý rằng đây không phải là một chuyển đổi thông thường, vì nó phải được thực hiện ở cấp độ cú pháp: biến thời gian chạy một giá trị của loại T thành một hàm sẽ không được sử dụng vì việc đánh giá đã được thực hiện.
Marc van Leeuwen

13

Với hợp lý hóa hồi cứu, chủ yếu là vì

  • để đảm bảo đoản mạch (không đưa ra cú pháp mới), các toán tử sẽ phải được giới hạn ở các kết quảđối số đầu tiên thực tế có thể chuyển đổi thành bool

  • đoản mạch có thể dễ dàng thể hiện theo những cách khác, khi cần thiết.


Ví dụ, nếu một lớp Tcó liên kết &&||toán tử, thì biểu thức

auto x = a && b || c;

trong đó a, bclà các biểu thức của loại T, có thể được biểu thị bằng ngắn mạch như

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);

hoặc có lẽ rõ ràng hơn như

auto x = [&]() -> T_op_result
{
    auto&& and_arg = a;
    auto&& and_result = (and_arg? and_arg && b : and_arg);
    if( and_result ) { return and_result; } else { return and_result || b; }
}();

Sự dư thừa rõ ràng bảo tồn bất kỳ tác dụng phụ nào từ các yêu cầu vận hành.


Trong khi lambda viết lại dài dòng hơn, đóng gói tốt hơn của nó cho phép người ta xác định các toán tử như vậy.

Tôi không hoàn toàn chắc chắn về sự phù hợp tiêu chuẩn của tất cả những điều sau đây (vẫn còn một chút ảnh hưởng), nhưng nó biên dịch hoàn toàn với Visual C ++ 12.0 (2013) và MinGW g ++ 4.8.2:

#include <iostream>
using namespace std;

void say( char const* s ) { cout << s; }

struct S
{
    using Op_result = S;

    bool value;
    auto is_true() const -> bool { say( "!! " ); return value; }

    friend
    auto operator&&( S const a, S const b )
        -> S
    { say( "&& " ); return a.value? b : a; }

    friend
    auto operator||( S const a, S const b )
        -> S
    { say( "|| " ); return a.value? a : b; }

    friend
    auto operator<<( ostream& stream, S const o )
        -> ostream&
    { return stream << o.value; }

};

template< class T >
auto is_true( T const& x ) -> bool { return !!x; }

template<>
auto is_true( S const& x ) -> bool { return x.is_true(); }

#define SHORTED_AND( a, b ) \
[&]() \
{ \
    auto&& and_arg = (a); \
    return (is_true( and_arg )? and_arg && (b) : and_arg); \
}()

#define SHORTED_OR( a, b ) \
[&]() \
{ \
    auto&& or_arg = (a); \
    return (is_true( or_arg )? or_arg : or_arg || (b)); \
}()

auto main()
    -> int
{
    cout << boolalpha;
    for( int a = 0; a <= 1; ++a )
    {
        for( int b = 0; b <= 1; ++b )
        {
            for( int c = 0; c <= 1; ++c )
            {
                S oa{!!a}, ob{!!b}, oc{!!c};
                cout << a << b << c << " -> ";
                auto x = SHORTED_OR( SHORTED_AND( oa, ob ), oc );
                cout << x << endl;
            }
        }
    }
}

Đầu ra:

000 -> !! !! | | sai
001 -> !! !! | | thật
010 -> !! !! | | sai
011 -> !! !! | | thật
100 -> !! && !! | | sai
101 -> !! && !! | | thật
110 -> !! && !! thật
111 -> !! && !! thật

Ở đây mỗi !!bang-bang hiển thị một chuyển đổi thành bool, tức là kiểm tra giá trị đối số.

Vì trình biên dịch có thể dễ dàng thực hiện tương tự và tối ưu hóa nó, đây là một triển khai có thể được chứng minh và mọi yêu cầu về tính không thể áp dụng phải được đặt trong cùng loại với các yêu cầu không thể nói chung, nói chung là các bollocks.


Tôi thích sự thay thế ngắn mạch của bạn, đặc biệt là người thay thế, gần như bạn có thể nhận được.
iFreilicht

Bạn đang thiếu tính ngắn mạch của &&- sẽ cần phải có một dòng bổ sung như if (!a) { return some_false_ish_T(); }- và với viên đạn đầu tiên của bạn: ngắn mạch là về các tham số có thể chuyển đổi thành bool, không phải là kết quả.
Arne Mertz

@ArneMertz: bình luận của bạn về "Mất tích" rõ ràng là vô nghĩa. các bình luận về những gì nó nói về, vâng tôi biết điều đó. chuyển đổi để boollà cần thiết để làm ngắn mạch.
Chúc mừng và hth. - Alf

@ Cheersandhth.-Alf bình luận về việc mất tích là cho lần sửa đổi đầu tiên của câu trả lời của bạn, nơi bạn đã làm ngắn mạch ||nhưng không phải là &&. Nhận xét khác là nhằm mục đích "sẽ phải giới hạn kết quả có thể chuyển đổi thành bool" trong dấu đầu dòng đầu tiên của bạn - nó nên đọc "giới hạn đối với tham số có thể chuyển đổi thành bool" imo.
Arne Mertz

@ArneMertz: OK, phiên bản lại, xin lỗi tôi đang chỉnh sửa chậm. Bị hạn chế, không phải là kết quả của toán tử phải bị hạn chế, bởi vì nó phải được chuyển đổi thành boolđể kiểm tra sự lưu thông ngắn của các toán tử tiếp theo trong biểu thức. Giống như, kết quả của a && bphải được chuyển đổi boolđể kiểm tra tính lưu hành ngắn của logic OR trong a && b || c.
Chúc mừng và hth. - Alf

5

tl; dr : nó không đáng để nỗ lực, do nhu cầu rất thấp (ai sẽ sử dụng tính năng này?) so với chi phí khá cao (cần cú pháp đặc biệt).

Điều đầu tiên xuất hiện là quá tải toán tử chỉ là một cách thú vị để viết các hàm, trong khi phiên bản boolean của các toán tử ||&&là công cụ buitlin. Điều đó có nghĩa là trình biên dịch có quyền tự do ngắn mạch chúng, trong khi biểu thức x = y && zvới nonboolean yzphải dẫn đến một cuộc gọi đến một chức năng như thế nào X operator&& (Y, Z). Điều này có nghĩa là đó y && zchỉ là một cách thú vị để viết operator&&(y,z)mà chỉ là một cuộc gọi của một hàm có tên kỳ quặc trong đó cả hai tham số phải được ước tính trước khi gọi hàm (bao gồm mọi thứ có thể coi là ngắn mạch).

Tuy nhiên, người ta có thể lập luận rằng có thể làm cho việc dịch thuật &&toán tử trở nên phức tạp hơn một chút, giống như đối với newtoán tử được dịch thành gọi hàm operator newtheo sau là lệnh gọi của hàm tạo.

Về mặt kỹ thuật, điều này sẽ không có vấn đề gì, người ta sẽ phải xác định một cú pháp ngôn ngữ cụ thể cho điều kiện tiên quyết cho phép đoản mạch. Tuy nhiên, việc sử dụng ngắn mạch sẽ bị hạn chế trong các trường hợp có thể chấp nhận Yđược X, nếu không thì phải có thêm thông tin về cách thực sự thực hiện đoản mạch (tức là tính kết quả từ chỉ tham số đầu tiên). Kết quả sẽ phải trông giống như thế này:

X operator&&(Y const& y, Z const& z)
{
  if (shortcircuitCondition(y))
    return shortcircuitEvaluation(y);

  <"Syntax for an evaluation-Point for z here">

  return actualImplementation(y,z);
}

Một người hiếm khi muốn quá tải operator||operator&&, vì hiếm khi có trường hợp viết a && bthực sự là trực quan trong một bối cảnh phi ngôn ngữ. Các ngoại lệ duy nhất tôi biết là các mẫu biểu thức, ví dụ như đối với DSL nhúng. Và chỉ một số ít các trường hợp đó sẽ được hưởng lợi từ đánh giá ngắn mạch. Các mẫu biểu thức thường không có, vì chúng được sử dụng để tạo các cây biểu thức được đánh giá sau này, vì vậy bạn luôn cần cả hai mặt của biểu thức.

Nói tóm lại: các tác giả biên dịch và tiêu chuẩn không phải là tác giả tiêu chuẩn cảm thấy cần phải nhảy qua các vòng và định nghĩa và thực hiện cú pháp rườm rà bổ sung, chỉ vì một trong một triệu người có thể hiểu rằng sẽ rất tốt nếu có sự cố ngắn mạch đối với người dùng được xác định operator&&operator||- chỉ để đi đến kết luận rằng nó không phải là nỗ lực ít hơn so với việc viết logic trên mỗi bàn tay.


Là chi phí thực sự cao? Ngôn ngữ lập trình D cho phép khai báo các tham số khi lazybiến biểu thức được cung cấp dưới dạng đối số ngầm thành hàm ẩn danh. Điều này cung cấp cho hàm được gọi là sự lựa chọn để gọi đối số đó hoặc không. Vì vậy, nếu ngôn ngữ đã có lambdas, cú pháp thêm cần thiết là rất nhỏ. Pseudocode ném đá: X và (A a, lười biếng B b) {if (cond (a)) {return short (a); } other {thực tế (a, b ()); }}
BlackJack

@BlackJack rằng tham số lười biếng có thể được thực hiện bằng cách chấp nhận một std::function<B()>, điều này sẽ phải chịu một chi phí nhất định. Hoặc nếu bạn sẵn sàng để nội tuyến nó làm cho nó template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. Và có thể quá tải nó với Btham số "bình thường" , vì vậy người gọi có thể quyết định chọn phiên bản nào. Các lazycú pháp có thể được thuận tiện hơn, nhưng có một sự cân bằng hiệu suất nhất định.
Arne Mertz

1
Một trong những vấn đề với std::functionso với lazylà lần đầu tiên có thể được đánh giá nhiều lần. Một tham số lười biếng foođược sử dụng như foo+foovẫn chỉ được đánh giá một lần.
MSalters

"Việc sử dụng ngắn mạch sẽ bị hạn chế trong các trường hợp Y có thể chuyển sang X" ... không, nó bị hạn chế trong các trường hợp Xcó thể được tính dựa trên Ymột mình. Rất khác nhau. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. Trừ khi bạn đang sử dụng một cách sử dụng "chuyển đổi" rất ngẫu nhiên.
Vịt Mooing

1
@Sumant họ có thể. Nhưng bạn cũng có thể viết ra logic của một tùy chỉnh ngắn mạch operator&&bằng tay. Câu hỏi không phải là nếu nó có thể, mà tại sao không có một cách thuận tiện ngắn.
Arne Mertz

5

Lambdas không phải là cách duy nhất để giới thiệu sự lười biếng. Đánh giá lười biếng tương đối đơn giản khi sử dụng Mẫu biểu thức trong C ++. Không cần từ khóa lazyvà nó có thể được thực hiện trong C ++ 98. Cây biểu hiện đã được đề cập ở trên. Mẫu biểu hiện là cây biểu hiện của người đàn ông nghèo (nhưng thông minh). Bí quyết là chuyển đổi biểu thức thành một cây tức thời lồng nhau đệ quy của Exprmẫu. Cây được đánh giá riêng sau khi xây dựng.

Đoạn mã sau thực hiện ngắn mạch &&||toán tử cho lớp Smiễn là nó cung cấp logical_andlogical_orcác hàm miễn phí và nó có thể chuyển đổi thành bool. Mã này có trong C ++ 14 nhưng ý tưởng cũng được áp dụng trong C ++ 98. Xem ví dụ trực tiếp .

#include <iostream>

struct S
{
  bool val;

  explicit S(int i) : val(i) {}  
  explicit S(bool b) : val(b) {}

  template <class Expr>
  S (const Expr & expr)
   : val(evaluate(expr).val)
  { }

  template <class Expr>
  S & operator = (const Expr & expr)
  {
    val = evaluate(expr).val;
    return *this;
  }

  explicit operator bool () const 
  {
    return val;
  }
};

S logical_and (const S & lhs, const S & rhs)
{
    std::cout << "&& ";
    return S{lhs.val && rhs.val};
}

S logical_or (const S & lhs, const S & rhs)
{
    std::cout << "|| ";
    return S{lhs.val || rhs.val};
}


const S & evaluate(const S &s) 
{
  return s;
}

template <class Expr>
S evaluate(const Expr & expr) 
{
  return expr.eval();
}

struct And 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? logical_and(temp, evaluate(r)) : temp;
  }
};

struct Or 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? temp : logical_or(temp, evaluate(r));
  }
};


template <class Op, class LExpr, class RExpr>
struct Expr
{
  Op op;
  const LExpr &lhs;
  const RExpr &rhs;

  Expr(const LExpr& l, const RExpr & r)
   : lhs(l),
     rhs(r)
  {}

  S eval() const 
  {
    return op(lhs, rhs);
  }
};

template <class LExpr>
auto operator && (const LExpr & lhs, const S & rhs)
{
  return Expr<And, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator && (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<And, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

template <class LExpr>
auto operator || (const LExpr & lhs, const S & rhs)
{
  return Expr<Or, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator || (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<Or, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

std::ostream & operator << (std::ostream & o, const S & s)
{
  o << s.val;
  return o;
}

S and_result(S s1, S s2, S s3)
{
  return s1 && s2 && s3;
}

S or_result(S s1, S s2, S s3)
{
  return s1 || s2 || s3;
}

int main(void) 
{
  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << and_result(S{i}, S{j}, S{k}) << std::endl;

  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << or_result(S{i}, S{j}, S{k}) << std::endl;

  return 0;
}

5

Đoản mạch các toán tử logic được cho phép vì đó là "tối ưu hóa" trong đánh giá các bảng chân lý liên quan. Nó là một chức năng của chính logic và logic này được xác định.

Có thực sự có một lý do tại sao quá tải &&||không ngắn mạch?

Các toán tử logic quá tải tùy chỉnh không bắt buộc phải tuân theo logic của các bảng chân lý này.

Nhưng tại sao họ mất hành vi này khi quá tải?

Do đó toàn bộ chức năng cần phải được đánh giá theo bình thường. Trình biên dịch phải coi nó như một toán tử (hoặc hàm) quá tải bình thường và nó vẫn có thể áp dụng tối ưu hóa như với bất kỳ hàm nào khác.

Mọi người quá tải các toán tử logic vì nhiều lý do. Ví dụ; chúng có thể có ý nghĩa cụ thể trong một miền cụ thể không phải là logic "thông thường" mà mọi người đã quen thuộc.


4

Việc đoản mạch là do bảng chân lý của "và" và "hoặc". Làm thế nào bạn biết người dùng sẽ xác định thao tác nào và làm thế nào bạn biết bạn sẽ không phải đánh giá toán tử thứ hai?


Như đã đề cập trong các bình luận và trong câu trả lời @Ded repeatators, có thể có một tính năng ngôn ngữ bổ sung. Tôi biết rằng nó không hoạt động bây giờ. Câu hỏi của tôi là lý do đằng sau không có tính năng như vậy là gì.
iFreilicht

Chà, nó chắc chắn sẽ là một tính năng phức tạp, vì chúng ta phải mạo hiểm đoán về định nghĩa của người dùng về nó!
nj-ath

Điều gì về : (<condition>)sau khi khai báo toán tử để xác định một điều kiện mà tại đó đối số thứ hai không được ước tính?
iFreilicht

@iFreilicht: Bạn vẫn cần một cơ quan chức năng đơn nguyên thay thế.
MSalters

3

nhưng các toán tử cho bool có hành vi này, tại sao nó phải được giới hạn ở kiểu đơn này?

Tôi chỉ muốn trả lời một phần này. Lý do là các biểu thức tích hợp &&||biểu thức không được triển khai với các hàm như các toán tử quá tải.

Có logic ngắn mạch tích hợp theo sự hiểu biết của các nhà soạn nhạc về các biểu thức cụ thể là dễ dàng. Nó giống như bất kỳ luồng điều khiển tích hợp nào khác.

Nhưng thay vào đó, quá tải toán tử được thực hiện với các hàm, có các quy tắc cụ thể, một trong số đó là tất cả các biểu thức được sử dụng làm đối số được đánh giá trước khi hàm được gọi. Rõ ràng các quy tắc khác nhau có thể được xác định, nhưng đó là một công việc lớn hơn.


1
Tôi tự hỏi nếu có xem xét đã được đưa ra cho câu hỏi của việc quá tải của &&, ||,nên được cho phép? Việc C ++ không có cơ chế cho phép quá tải hoạt động giống như mọi thứ khác ngoài các lệnh gọi hàm giải thích tại sao quá tải các hàm đó không thể làm gì khác, nhưng điều đó không giải thích tại sao các toán tử đó quá tải ngay từ đầu. Tôi nghi ngờ lý do thực sự đơn giản là họ bị ném vào danh sách các nhà khai thác mà không cần suy nghĩ nhiều.
supercat
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.