Apakah sebenarnya ada alasan mengapa kelebihan beban && dan || jangan korsleting?


138

Perilaku korsleting dari operator &&dan ||merupakan alat yang luar biasa untuk programmer.

Tetapi mengapa mereka kehilangan perilaku ini saat kelebihan beban? Saya memahami bahwa operator hanyalah gula sintaksis untuk fungsi tetapi operator untuk boolmemiliki perilaku ini, mengapa harus dibatasi pada jenis tunggal ini? Apakah ada alasan teknis di balik ini?


1
@Pot. Pertanyaan itu mungkin adalah jawabannya. Saya kira standar dapat mendefinisikan sintaks baru hanya untuk tujuan ini. Mungkin seperti operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht

1
@PiotrS .: Pertimbangkan logika tri-state: {true, false, nil}. Karena nil&& x == nilbisa korsleting.
MSalters

1
@ MSalters: Pertimbangkan std::valarray<bool> a, b, c;, bagaimana menurut Anda a || b || cakan mengalami korsleting?
Piotr Skotnicki

4
@PiotrS .: Saya berpendapat bahwa terdapat setidaknya satu jenis non-bool untuk hubungan pendek yang masuk akal. Saya tidak berpendapat bahwa sirkuit pendek masuk akal untuk setiap jenis non-bool.
MSalters

3
Belum ada yang menyebutkan ini, tetapi ada juga masalah kompatibilitas ke belakang. Kecuali jika perhatian khusus diberikan untuk membatasi keadaan di mana korsleting ini akan berlaku, korsleting semacam itu dapat merusak kode yang ada yang kelebihan beban operator&&atau operator||dan bergantung pada kedua operan yang dievaluasi. Mempertahankan kompatibilitas ke belakang adalah (atau seharusnya) penting saat menambahkan fitur ke bahasa yang sudah ada.
David Hammen

Jawaban:


152

Semua proses desain menghasilkan kompromi antara tujuan yang saling tidak kompatibel. Sayangnya, proses desain untuk &&operator yang kelebihan beban di C ++ menghasilkan hasil akhir yang membingungkan: bahwa fitur yang Anda inginkan &&- perilaku hubung singkatnya - dihilangkan.

Detail tentang bagaimana proses desain itu berakhir di tempat malang ini, yang tidak saya ketahui. Namun relevan untuk melihat bagaimana proses desain selanjutnya memperhitungkan hasil yang tidak menyenangkan ini. Di C #, &&operator yang kelebihan beban mengalami korsleting. Bagaimana para desainer C # mencapai itu?

Salah satu jawaban lain menyarankan "pengangkatan lambda". Itu adalah:

A && B

dapat diwujudkan sebagai sesuatu yang secara moral setara dengan:

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

dimana argumen kedua menggunakan beberapa mekanisme untuk evaluasi malas sehingga ketika dievaluasi, efek samping dan nilai ekspresi yang dihasilkan. Implementasi dari operator yang kelebihan beban hanya akan melakukan evaluasi malas jika diperlukan.

Ini bukanlah yang dilakukan oleh tim desain C #. (Selain: meskipun pengangkatan lambda adalah apa yang saya lakukan ketika tiba waktunya untuk melakukan representasi pohon ekspresi dari ??operator, yang memerlukan operasi konversi tertentu untuk dilakukan dengan malas. Menjelaskan bahwa secara detail akan menjadi penyimpangan besar. Cukup untuk dikatakan: pengangkatan lambda berfungsi tetapi cukup kelas berat sehingga kami ingin menghindarinya.)

Sebaliknya, solusi C # memecah masalah menjadi dua masalah terpisah:

  • haruskah kita mengevaluasi operan kanan?
  • jika jawaban di atas adalah "ya", lalu bagaimana kita menggabungkan kedua operan tersebut?

Oleh karena itu, masalah diselesaikan dengan membuatnya ilegal untuk membebani &&secara langsung. Sebaliknya, di C # Anda harus membebani dua operator, yang masing-masing menjawab salah satu dari dua pertanyaan tersebut.

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 }
    ...

(Selain: sebenarnya, tiga. C # mensyaratkan bahwa jika operator falsedisediakan maka operator truejuga harus disediakan, yang menjawab pertanyaan: apakah hal ini "benar-benar?". Biasanya tidak ada alasan untuk menyediakan hanya satu operator tersebut jadi C # membutuhkan keduanya.)

Pertimbangkan pernyataan dalam bentuk:

C cresult = cleft && cright;

Kompilator menghasilkan kode untuk ini seperti yang Anda kira Anda telah menulis pseudo-C ini #:

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

Seperti yang Anda lihat, sisi kiri selalu dievaluasi. Jika ditentukan sebagai "false-ish" maka itulah hasilnya. Jika tidak, sisi kanan dievaluasi, dan operator yang ditentukan pengguna yang bersemangat& dipanggil.

The ||Operator didefinisikan dalam cara analog, sebagai doa operator yang benar dan bersemangat |Operator:

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

Dengan mendefinisikan semua empat operator - true, false, &dan |- C # memungkinkan Anda untuk tidak hanya mengatakan cleft && crighttetapi juga non-hubungan arus pendek cleft & cright, dan juga if (cleft) if (cright) ..., dan c ? consequence : alternativedan while(c), dan sebagainya.

Sekarang, saya mengatakan bahwa semua proses desain adalah hasil kompromi. Di sini perancang bahasa C # berhasil mendapatkan hubungan arus pendek &&dan ||kanan, tetapi melakukan hal itu memerlukan kelebihan beban empat operator, bukan dua , yang membingungkan beberapa orang. Fitur benar / salah operator adalah salah satu fitur yang paling tidak dipahami dengan baik di C #. Tujuan memiliki bahasa yang masuk akal dan lugas yang akrab bagi pengguna C ++ ditentang oleh keinginan untuk mengalami korsleting dan keinginan untuk tidak mengimplementasikan lambda lifting atau bentuk evaluasi malas lainnya. Saya pikir itu adalah posisi kompromi yang masuk akal, tetapi penting untuk disadari bahwa itu adalah posisi kompromi. Hanya berbeda posisi kompromi daripada desainer C ++ mendarat.

Jika subjek desain bahasa untuk operator semacam itu menarik minat Anda, pertimbangkan untuk membaca seri saya tentang mengapa C # tidak mendefinisikan operator ini pada nullable Boolean:

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


1
@Deduplicator: Anda mungkin juga tertarik untuk membaca pertanyaan dan jawaban ini: stackoverflow.com/questions/5965968/…
Eric Lippert

5
Dalam kasus ini, saya pikir kompromi itu lebih dari sekadar dibenarkan. Hal-hal rumit adalah sesuatu yang hanya arsitek dari perpustakaan kelas harus peduli dengan, dan dalam pertukaran untuk komplikasi ini, itu membuat konsumsi perpustakaan lebih mudah dan lebih intuitif.
Cody Grey

1
@EricLippert Saya yakin Envision menyatakan bahwa dia melihat posting ini dan mengira itu adalah Anda ... lalu melihat dia benar. Dia tidak mengatakan your postitu tidak relevan. His noticing your distinct writing styletidak relevan.
WernerCD

5
Tim Microsoft tidak mendapatkan kredit yang cukup untuk (1) melakukan upaya yang sangat baik untuk melakukan hal yang benar di C # dan (2) lebih sering melakukannya dengan benar.
codenheim

2
@Voo: Jika Anda memilih untuk menerapkan konversi implisit ke boolmaka Anda dapat menggunakan &&dan ||tanpa menerapkan operator true/falseatau operator &/|di C # tidak ada masalah. Masalahnya justru muncul dalam situasi di mana tidak ada konversi menjadi boolmungkin , atau di mana seseorang tidak diinginkan.
Eric Lippert

44

Intinya adalah bahwa (dalam batasan C ++ 98) operan kanan akan diteruskan ke fungsi operator yang kelebihan beban sebagai argumen. Dengan demikian, itu sudah dievaluasi . Tidak ada operator||()atau operator&&()kode bisa atau tidak bisa melakukan yang akan menghindari ini.

Operator asli berbeda, karena ini bukan fungsi, tetapi diimplementasikan pada level bahasa yang lebih rendah.

Fitur bahasa tambahan bisa membuat non-evaluasi operan kanan mungkin secara sintaksis . Namun, mereka tidak peduli karena hanya ada beberapa kasus tertentu di mana ini akan berguna secara semantik . (Sama seperti ? :, yang tidak tersedia untuk kelebihan beban sama sekali.

(Mereka butuh 16 tahun untuk memasukkan lambda ke dalam standar ...)

Adapun penggunaan semantik, pertimbangkan:

objectA && objectB

Ini bermuara pada:

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

Pikirkan tentang apa sebenarnya yang ingin Anda lakukan dengan objectB (jenis tidak diketahui) di sini, selain memanggil operator konversi ke bool, dan bagaimana Anda akan memasukkannya ke dalam kata-kata untuk definisi bahasa.

Dan jika Anda sedang menelepon konversi ke bool, baik ...

objectA && obectB

melakukan hal yang sama, sekarang bukan? Jadi mengapa membebani di tempat pertama?


7
baik kesalahan logika Anda adalah untuk alasan dalam bahasa yang didefinisikan saat ini tentang efek dari bahasa yang didefinisikan secara berbeda. di masa lalu banyak pemula biasa melakukan itu. "konstruktor virtual". butuh banyak sekali penjelasan untuk mengeluarkan mereka dari pemikiran kotak seperti itu. bagaimanapun, dengan hubungan pendek dari operator bawaan ada jaminan tentang argumen non-evaluasi. jaminan semacam itu juga akan ada untuk kelebihan beban yang ditentukan pengguna, jika hubungan pendek ditentukan untuk mereka.
Cheers and hth. - Alf

1
@iFreilicht: Saya pada dasarnya mengatakan hal yang sama dengan Deduplicator atau Piotr, hanya dengan kata-kata yang berbeda. Saya menguraikan sedikit tentang poin dalam jawaban yang diedit. Jauh lebih nyaman dengan cara ini, ekstensi bahasa yang diperlukan (misalnya lambda) tidak ada hingga saat ini, dan manfaatnya tetap dapat diabaikan. Beberapa kali di mana orang yang bertanggung jawab akan "menyukai" sesuatu yang belum dilakukan oleh pembuat kompiler, pada tahun 1998, itu menjadi bumerang. (Lihat export.)
DevSolar

9
@iFreilicht: boolOperator konversi untuk kedua kelas juga memiliki akses ke semua variabel anggota, dan berfungsi dengan baik dengan operator bawaan. Ada lagi selain konversi-ke-bool tidak masuk akal semantik untuk evaluasi sirkuit pendek! Cobalah untuk mendekati ini dari sudut pandang semantik, bukan dari sudut pandang sintaksis: Apa yang ingin Anda capai, bukan bagaimana Anda akan melakukannya.
DevSolar

1
Saya harus mengakui bahwa saya tidak dapat memikirkannya. Satu-satunya alasan terjadinya korsleting adalah karena ia menghemat waktu untuk operasi pada Boolean dan Anda dapat mengetahui hasil ekspresi sebelum semua argumen dievaluasi. Dengan operasi AND lainnya, bukan itu masalahnya, dan itulah mengapa &dan &&bukan operator yang sama. Terima kasih telah membantu saya menyadarinya.
iFreilicht

8
@iFreilicht: Sebaliknya, tujuan korsleting adalah karena penghitungan sisi kiri dapat menetapkan kebenaran prasyarat sisi kanan . if (x != NULL && x->foo)membutuhkan korsleting, bukan untuk kecepatan, tetapi untuk keamanan.
Eric Lippert

27

Sebuah fitur harus dipikirkan, dirancang, diimplementasikan, didokumentasikan dan dikirim.

Sekarang kita memikirkannya, mari kita lihat mengapa sekarang mungkin mudah (dan sulit untuk dilakukan kemudian). Juga perlu diingat bahwa hanya ada sejumlah sumber daya, jadi menambahkannya mungkin akan memotong sesuatu yang lain (Apa yang ingin Anda tinggalkan?).


Secara teori, semua operator dapat mengizinkan perilaku short-circuiting dengan hanya satu fitur bahasa tambahan "minor" , mulai dari C ++ 11 (ketika lambda diperkenalkan, 32 tahun setelah "C dengan kelas" dimulai pada tahun 1979, 16 setelah c ++ 98):

C ++ hanya memerlukan cara untuk menganotasi argumen sebagai lazy -aluation - a hidden-lambda - untuk menghindari evaluasi hingga diperlukan dan diizinkan (prasyarat terpenuhi).


Seperti apa fitur teoritis itu (Ingat bahwa fitur baru harus dapat digunakan secara luas)?

Anotasi lazy, yang diterapkan ke fungsi-argumen membuat fungsi template mengharapkan functor, dan membuat kompilator mengemas ekspresi menjadi functor:

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

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

Ini akan terlihat di bawah sampul seperti:

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;});

Perhatikan secara khusus bahwa lambda tetap tersembunyi, dan akan dipanggil paling banyak sekali.
Seharusnya tidak ada penurunan kinerja karena hal ini, selain dari berkurangnya peluang eliminasi subekspresi umum.


Selain kompleksitas implementasi dan kompleksitas konseptual (setiap fitur meningkatkan keduanya, kecuali jika cukup memudahkan kompleksitas tersebut untuk beberapa fitur lain), mari kita lihat pertimbangan penting lainnya: Kompatibilitas mundur.

Meskipun fitur bahasa ini tidak akan merusak kode apa pun, fitur ini akan secara halus mengubah API apa pun yang memanfaatkannya, yang berarti penggunaan apa pun di pustaka yang ada akan menjadi perubahan pemutusan yang diam-diam.

BTW: Fitur ini, meskipun lebih mudah digunakan, lebih kuat daripada solusi pemisahan C # &&dan ||menjadi dua fungsi masing-masing untuk definisi terpisah.


6
@iFreilicht: Ada pertanyaan dalam bentuk "mengapa fitur X tidak ada?" memiliki jawaban yang sama: agar ada fitur yang harus dipikirkan, dianggap sebagai ide yang baik, dirancang, ditentukan, diimplementasikan, diuji, didokumentasikan, dan dikirim ke pengguna akhir. Jika salah satu dari hal itu tidak terjadi, tidak ada fitur. Salah satu hal tersebut tidak terjadi dengan fitur yang Anda usulkan; mencari tahu mana yang menjadi masalah penelitian sejarah; mulailah berbicara dengan orang-orang di komite desain jika Anda peduli yang mana yang tidak pernah dilakukan.
Eric Lippert

1
@EricLippert: Dan, tergantung pada alasannya, ulangi sampai diterapkan: Mungkin dianggap terlalu rumit, dan tidak ada yang berpikir untuk melakukan evaluasi ulang. Atau evaluasi ulang diakhiri dengan alasan penolakan yang berbeda dari yang diadakan sebelumnya. (btw: Menambahkan inti komentar Anda)
Deduplicator

@Deduplicator Dengan template ekspresi, baik kata kunci lazy maupun lambda tidak diperlukan.
Sumant

Sebagai tambahan historis, perhatikan bahwa bahasa Algol 68 asli memiliki paksaan "prosedural" (serta deproceduring, yang berarti secara implisit memanggil fungsi tanpa parameter ketika konteksnya membutuhkan jenis hasil daripada jenis fungsi). Ini berarti bahwa ekspresi tipe T dalam posisi yang membutuhkan nilai tipe "fungsi parameterless yang mengembalikan T" (dieja " proc T" dalam Algol 68) akan secara implisit diubah menjadi badan fungsi yang mengembalikan ekspresi yang diberikan (lambda implisit). Fitur tersebut telah dihapus (tidak seperti deproceduring) dalam revisi bahasa tahun 1973.
Marc van Leeuwen

... Untuk C ++, pendekatan serupa bisa jadi dengan mendeklarasikan operator seperti &&mengambil satu argumen tipe "pointer ke fungsi mengembalikan T" dan aturan konversi tambahan yang memungkinkan ekspresi argumen tipe T secara implisit diubah menjadi ekspresi lambda. Perhatikan bahwa ini bukan konversi biasa, karena harus dilakukan pada tingkat sintaksis: mengubah pada waktu proses nilai tipe T menjadi fungsi tidak akan berguna karena evaluasi sudah dilakukan.
Marc van Leeuwen

13

Dengan rasionalisasi retrospektif, terutama karena

  • dalam rangka untuk menjamin hubungan pendek (tanpa memperkenalkan sintaks baru) operator harus dibatasi hasilargumen pertama aktual dapat diubah menjadi bool, dan

  • korsleting dapat dengan mudah diekspresikan dengan cara lain, bila diperlukan.


Misalnya, jika kelas Tmemiliki asosiasi &&dan ||operator, maka ekspresi

auto x = a && b || c;

di mana a, bdan cadalah ekspresi tipe T, dapat diekspresikan dengan hubung singkat sebagai

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

atau mungkin lebih jelas

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; }
}();

Redundansi yang terlihat mempertahankan efek samping dari panggilan operator.


Sementara lambda rewrite lebih bertele-tele, enkapsulasi yang lebih baik memungkinkan seseorang untuk mendefinisikan operator tersebut.

Saya tidak sepenuhnya yakin dengan kesesuaian standar dari semua berikut ini (masih sedikit influensa), tetapi dikompilasi dengan rapi dengan Visual C ++ 12.0 (2013) dan 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;
            }
        }
    }
}

Keluaran:

000 -> !! !! || Salah
001 -> !! !! || benar
010 -> !! !! || Salah
011 -> !! !! || benar
100 -> !! && !! || Salah
101 -> !! && !! || benar
110 -> !! && !! benar
111 -> !! && !! benar

Di sini setiap !!bang-bang menunjukkan konversi ke bool, yaitu pemeriksaan nilai argumen.

Karena kompiler dapat dengan mudah melakukan hal yang sama, dan sebagai tambahan mengoptimalkannya, ini adalah implementasi yang mungkin didemonstrasikan dan setiap klaim ketidakmungkinan harus dimasukkan ke dalam kategori yang sama dengan klaim ketidakmungkinan pada umumnya, yaitu, umumnya omong kosong.


Saya suka substitusi korsleting Anda, terutama yang terner, yang sedekat mungkin bisa Anda dapatkan.
iFreilicht

Anda kehilangan korsleting dari &&- perlu ada garis tambahan seperti if (!a) { return some_false_ish_T(); }- dan ke poin pertama Anda: korsleting adalah tentang parameter yang dapat diubah menjadi bool, bukan hasilnya.
Arne Mertz

@ArneMertz: komentar Anda tentang "Hilang" tampaknya tidak ada artinya. komentar tentang apa itu, ya saya menyadarinya. konversi ke booldiperlukan untuk melakukan hubungan arus pendek.
Cheers and hth. - Alf

@ Cheersandhth.-Alf komentar tentang hilang adalah untuk revisi pertama dari jawaban Anda di mana Anda melakukan korsleting ||tetapi tidak &&. Komentar lain ditujukan pada imo " harus dibatasi pada hasil yang dapat diubah menjadi bool" di poin pertama Anda - harus dibaca "terbatas pada parameter yang dapat diubah menjadi bool".
Arne Mertz

@ArneMertz: Oke, versi ulang, maaf saya lambat mengedit. Dibatasi ulang, bukan hasil operator yang harus dibatasi, karena harus dikonversi ke booluntuk memeriksa korsleting operator lebih lanjut dalam ekspresi tersebut. Seperti, hasil dari a && bharus dikonversi ke booluntuk memeriksa korsleting dari logika OR di a && b || c.
Cheers and hth. - Alf

6

tl; dr : tidak sepadan dengan usaha, karena permintaan yang sangat rendah (siapa yang akan menggunakan fitur tersebut?) dibandingkan dengan biaya yang agak tinggi (diperlukan sintaks khusus).

Hal pertama yang terlintas dalam pikiran adalah bahwa operator overloading hanyalah cara yang bagus untuk menulis fungsi, sedangkan versi boolean dari operator ||dan &&merupakan barang buitlin. Itu berarti bahwa compiler memiliki kebebasan untuk melakukan short-circuit, sedangkan ekspresi x = y && zdengan nonboolean ydan zharus mengarah ke pemanggilan fungsi seperti X operator&& (Y, Z). Ini berarti itu y && zhanyalah cara yang bagus untuk menulis operator&&(y,z)yang hanya merupakan panggilan dari fungsi bernama aneh di mana kedua parameter harus dievaluasi sebelum memanggil fungsi (termasuk apa pun yang akan dianggap sesuai dengan hubungan arus pendek).

Namun, orang dapat berargumen bahwa itu harus memungkinkan untuk membuat terjemahan &&operator agak lebih canggih, seperti untuk newoperator yang diterjemahkan ke dalam memanggil fungsi yang operator newdiikuti oleh panggilan konstruktor.

Secara teknis ini tidak akan menjadi masalah, seseorang harus mendefinisikan sintaks bahasa khusus untuk prasyarat yang memungkinkan terjadinya hubungan arus pendek. Namun, penggunaan hubung singkat akan dibatasi untuk kasus-kasus di mana Ydapat diterima X, atau harus ada info tambahan tentang bagaimana sebenarnya melakukan korsleting (yaitu menghitung hasil hanya dari parameter pertama). Hasilnya akan terlihat seperti ini:

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);
}

Jarang ada yang ingin membebani operator||dan operator&&, karena jarang ada kasus di mana menulis a && bsebenarnya intuitif dalam konteks nonboolean. Satu-satunya pengecualian yang saya ketahui adalah template ekspresi, misalnya untuk DSL yang disematkan. Dan hanya segelintir dari sedikit kasus itu yang akan mendapat manfaat dari evaluasi korsleting. Templat ekspresi biasanya tidak, karena digunakan untuk membentuk pohon ekspresi yang dievaluasi nanti, jadi Anda selalu membutuhkan kedua sisi ekspresi.

Singkatnya: baik penulis kompiler maupun penulis standar merasa perlu untuk melompati rintangan dan mendefinisikan serta mengimplementasikan sintaks tambahan yang rumit, hanya karena satu dari sejuta mungkin mendapatkan gagasan bahwa alangkah baiknya memiliki arus pendek pada yang ditentukan pengguna operator&&dan operator||- hanya untuk sampai pada kesimpulan bahwa itu tidak kurang dari usaha menulis logika per tangan.


Apakah biayanya memang setinggi itu? Bahasa pemrograman D memungkinkan untuk mendeklarasikan parameter lazyyang mengubah ekspresi yang diberikan sebagai argumen secara implisit menjadi fungsi anonim. Ini memberi fungsi yang dipanggil pilihan untuk memanggil argumen itu, atau tidak. Jadi jika bahasa sudah memiliki lambda, sintaks tambahan yang dibutuhkan sangat kecil. "Pseudocode": X dan (A a, lazy B b) {if (cond (a)) {return short (a); } lain {aktual (a, b ()); }}
BlackJack

@BlackJack bahwa parameter malas dapat diimplementasikan dengan menerima a std::function<B()>, yang akan menimbulkan overhead tertentu. Atau jika Anda mau sebaris itu membuatnya template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. Dan mungkin membebani dengan Bparameter "normal" , sehingga pemanggil dapat memutuskan versi mana yang akan dipilih. The lazysintaks mungkin lebih nyaman namun memiliki tradeoff kinerja tertentu.
Arne Mertz

1
Salah satu masalah dengan std::functionversus lazyadalah bahwa yang pertama dapat dievaluasi beberapa kali. Parameter malas fooyang digunakan sebagai foo+foomasih hanya dievaluasi sekali.
MSalters

"penggunaan sirkuit pendek akan dibatasi untuk kasus-kasus di mana Y dapat disesuaikan dengan X" ... tidak, itu terbatas pada kasus-kasus di mana Xdapat dihitung berdasarkan Ysendiri. Sangat berbeda. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. Kecuali jika Anda menggunakan penggunaan "konversi" yang sangat biasa.
Mooing Duck

1
@Umant mereka bisa. Tapi Anda juga bisa menulis logika kebiasaan korsleting operator&&dengan tangan. Pertanyaannya bukanlah apakah itu mungkin, tetapi mengapa tidak ada jalan pintas yang nyaman.
Arne Mertz

6

Lambdas bukanlah satu-satunya cara untuk memperkenalkan kemalasan. Evaluasi malas relatif langsung menggunakan Template Ekspresi di C ++. Tidak perlu kata kunci lazydan dapat diterapkan di C ++ 98. Pohon ekspresi sudah disebutkan di atas. Templat ekspresi adalah pohon ekspresi orang yang buruk (tapi pintar). Triknya adalah dengan mengubah ekspresi menjadi pohon instance Exprtemplate yang bertingkat secara rekursif . Pohon dievaluasi secara terpisah setelah konstruksi.

Kode berikut mengimplementasikan hubung singkat &&dan ||operator untuk kelas Sselama kode tersebut menyediakan logical_anddan logical_orfungsi gratis serta dapat diubah menjadi bool. Kode ada di C ++ 14 tetapi idenya dapat diterapkan di C ++ 98 juga. Lihat contoh langsung .

#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;
}

6

Hubung singkat operator logika diperbolehkan karena ini merupakan "pengoptimalan" dalam evaluasi tabel kebenaran yang terkait. Ini adalah fungsi dari logika itu sendiri, dan logika ini didefinisikan.

Apakah sebenarnya ada alasan mengapa kelebihan beban &&dan ||tidak korsleting?

Operator logika kustom yang kelebihan beban tidak diwajibkan untuk mengikuti logika tabel kebenaran ini.

Tetapi mengapa mereka kehilangan perilaku ini saat kelebihan beban?

Oleh karena itu, seluruh fungsi perlu dievaluasi seperti biasa. Compiler harus memperlakukannya sebagai operator (atau fungsi) yang kelebihan beban normal dan masih dapat menerapkan pengoptimalan seperti pada fungsi lainnya.

Orang membebani operator logika karena berbagai alasan. Sebagai contoh; mereka mungkin memiliki arti khusus dalam domain tertentu yang bukan logika "normal" yang biasa digunakan orang.


5

Korsleting ini karena tabel kebenaran "dan" dan "atau". Bagaimana Anda tahu operasi apa yang akan didefinisikan pengguna dan bagaimana Anda tahu bahwa Anda tidak perlu mengevaluasi operator kedua?


Seperti yang disebutkan dalam komentar dan jawaban @Deduplicators, itu dimungkinkan dengan fitur bahasa tambahan. Saya tahu itu tidak berhasil sekarang. Pertanyaan saya adalah apa alasan di balik tidak adanya fitur seperti itu.
iFreilicht

Yah, itu pasti akan menjadi fitur yang rumit, mengingat kami harus menebak-nebak tentang definisi pengguna tentang itu!
nj-ath

Bagaimana dengan : (<condition>)setelah deklarasi operator untuk menentukan kondisi dimana argumen kedua tidak dievaluasi?
iFreilicht

@iFreilicht: Anda masih membutuhkan badan fungsi unary alternatif.
MSalters

3

tetapi operator untuk bool memiliki perilaku ini, mengapa harus dibatasi untuk tipe tunggal ini?

Saya hanya ingin menjawab bagian yang satu ini. Alasannya adalah bahwa ekspresi &&dan ||bawaan tidak diimplementasikan dengan fungsi seperti operator yang kelebihan beban.

Memiliki logika hubung singkat bawaan untuk pemahaman kompiler tentang ekspresi tertentu itu mudah. Ini sama seperti aliran kontrol bawaan lainnya.

Tetapi kelebihan beban operator diimplementasikan dengan fungsi, yang memiliki aturan tertentu, salah satunya adalah bahwa semua ekspresi yang digunakan sebagai argumen dievaluasi sebelum fungsi dipanggil. Aturan yang jelas berbeda dapat didefinisikan, tapi itu pekerjaan yang lebih besar.


1
Aku ingin tahu apakah pertimbangan diberikan kepada pertanyaan apakah overloads dari &&, ||dan ,harus diperbolehkan? Fakta bahwa C ++ tidak memiliki mekanisme untuk memungkinkan kelebihan beban berperilaku seperti apa pun selain panggilan fungsi menjelaskan mengapa kelebihan beban fungsi tersebut tidak dapat melakukan hal lain, tetapi tidak menjelaskan mengapa operator tersebut dapat kelebihan beban sejak awal. Saya menduga alasan sebenarnya adalah karena mereka dimasukkan ke dalam daftar operator tanpa banyak berpikir.
supercat
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.