Aslında aşırı yüklenmenin bir nedeni var mı? kısa devre yok mu?


137

Operatörlerin kısa devre davranışı &&ve ||programcılar için harika bir araçtır.

Fakat aşırı yüklendiğinde neden bu davranışı kaybediyorlar? Operatörlerin fonksiyonlar için sadece sözdizimsel şeker olduğunu anlıyorum, ancak operatörler boolbu davranışa sahipler, neden bu tek tiple sınırlandırılmalı? Bunun arkasında herhangi bir teknik gerekçe var mı?


1
@PiotrS. Bu soru muhtemelen cevaptır. Sanırım standart sadece bu amaç için yeni bir sözdizimi tanımlayabilir. Muhtemelen beğenmek operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht

1
@PiotrS tri-devlet mantığını düşünün .:: {true, false, nil}. nil&& x == nilKısa devre yapabildiğinden beri .
MSalters

1
@MSalters: Kısa devre std::valarray<bool> a, b, c;yapmayı nasıl hayal edersiniz a || b || c?
Piotr Skotnicki

4
@PiotrS .: Ben kısa devre mantıklı için en az bir bool olmayan türü olduğunu savunuyorum . Kısa dolaşımın, bool olmayan her tip için mantıklı olduğunu iddia etmiyorum .
MSalters

3
Henüz kimse bundan bahsetmedi, ancak geriye dönük uyumluluk sorunu da var. Bu kısa devrenin uygulanacağı durumları sınırlamaya özel bir özen gösterilmediği sürece, bu kısa devre, aşırı yüklenen operator&&veya operator||değerlendirilen her iki işlenene bağlı olan mevcut kodu kırabilir . Mevcut bir dile özellikler eklerken geriye dönük uyumluluğu korumak önemlidir (veya olmalıdır).
David Hammen

Yanıtlar:


151

Tüm tasarım süreçleri, karşılıklı uyumsuz hedefler arasında uzlaşmaya yol açar. Ne yazık ki, &&C ++ ' da aşırı yüklenmiş operatörün tasarım süreci kafa karıştırıcı bir sonuç verdi: İstediğiniz özellik &&- kısa devre davranışı - atlandı.

Tasarım sürecinin bu talihsiz yerde, bilmediğim yerde nasıl sonuçlandığına dair detaylar. Bununla birlikte, daha sonraki bir tasarım sürecinin bu hoş olmayan sonucu nasıl dikkate aldığını görmek önemlidir. C # aşırı &&operatör olan kısa devre. C # tasarımcıları bunu nasıl başardılar?

Diğer cevaplardan biri "lambda kaldırma" yı gösteriyor. Yani:

A && B

ahlaken eşdeğer bir şey olarak gerçekleştirilebilir:

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

burada ikinci argüman tembel değerlendirme için bir mekanizma kullanır, böylece değerlendirildiğinde ifadenin yan etkileri ve değeri üretilir. Aşırı yüklenmiş operatörün uygulanması, tembel değerlendirmeyi yalnızca gerektiğinde yapacaktır.

C # tasarım ekibinin yaptığı bu değil. (Kenara: lambda kaldırma olsa olduğunu bunu yapmak için zamanı geldiğinde ne yaptığını ifade ağacı temsil ait ??.. Belirli dönüşüm işlemlerini gerektirir operatörü, tembel yapılacak ayrıntılı olarak ancak büyük bir Arasöz olacağını anlatan yeterli demek: lambda kaldırma çalışıyor ancak bundan kaçınmak istediğimiz kadar ağır.)

Aksine, C # çözümü sorunu iki ayrı soruna ayırır:

  • sağ taraftaki işleneni değerlendirmeli miyiz?
  • yukarıdakilerin cevabı "evet" ise, iki işleneni nasıl birleştirebiliriz?

Bu nedenle, &&doğrudan aşırı yüklenmeyi yasa dışı hale getirerek sorun çözülür . Aksine, C # 'da , her biri bu iki sorudan birini yanıtlayan iki işleç aşırı yüklenmelidir .

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

(Bir kenara: aslında, üç. C #, operatörün falsesağlanması durumunda, operatörün de sağlanması truegerektiğini gerektirir , bu da soruyu cevaplar: bu şey "true-ish?" Dir.Genellikle böyle bir operatörü sağlamak için hiçbir neden olmaz. her ikisini de gerektirir.)

Formun bir ifadesini düşünün:

C cresult = cleft && cright;

Derleyici, bu sözde-C # yazdığınızı düşündüğü gibi bunun için kod üretir:

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

Gördüğünüz gibi, sol taraf daima değerlendirilir. Eğer "false-ish" olduğu belirlenirse, sonuç budur. Aksi takdirde, sağ taraf değerlendirilir ve istekli kullanıcı tanımlı operatör &çağrılır.

||Operatör bir operatörün çağırma doğru ve istekli olarak, benzer bir şekilde tanımlanır |operatör:

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

Dört operatörleri tanımlayarak - true, false, &ve |- C # söylemek sadece verir cleft && crightolmayan kısa devre değil, aynı zamanda cleft & crightve aynı zamanda if (cleft) if (cright) ...ve c ? consequence : alternativeve while(c)ve böyle devam eder.

Şimdi, tüm tasarım süreçlerinin uzlaşmanın sonucu olduğunu söyledim. Burada C # dil tasarımcıları kısa devre &&ve ||doğru olmayı başardı , ancak bunu yapmak için iki yerine dört operatörün aşırı yüklenmesi gerekiyor , bu da bazı insanların kafa karıştırıcı buluyor. Operatör true / false özelliği, C # 'da en az anlaşılan özelliklerden biridir. C ++ kullanıcılarına aşina olan mantıklı ve anlaşılır bir dile sahip olma amacına kısa devre yapma arzusu ve lambda kaldırma veya diğer tembel değerlendirme biçimlerini uygulama isteği karşı çıkmıştır. Bunun makul bir uzlaşmaya pozisyon olduğunu düşünüyorum, ama buna fark etmek önemlidir olduğu bir uzlaşma pozisyonu. Sadece farklı C ++ tasarımcılarına göre daha uzlaşma.

Bu tür operatörler için dil tasarımı konusu sizi ilgilendiriyorsa, C # 'ın nulllable Booleans üzerinde bu operatörleri neden tanımlamadığına dair dizimi okumayı düşünün:

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


1
@Deduplicator: Bu soru ve cevapları okumak da ilginizi çekebilir: stackoverflow.com/questions/5965968/…
Eric Lippert

5
Bu durumda, uzlaşmanın haklı olmaktan daha fazlası olduğunu düşünüyorum. Karmaşık şeyler, sadece bir sınıf kütüphanesinin mimarı ile ilgilenilmesi gereken bir şeydir ve bu komplikasyon karşılığında kütüphanenin tüketimini daha kolay ve sezgisel hale getirir .
Cody Gray

1
@EricLippert Envision'un bu gönderiyi gördüğünü ve senin olduğunu düşündüğünü söylediğine inanıyorum ... sonra haklı olduğunu gördü. your postAlakasız olduğunu söylemiyordu . His noticing your distinct writing styleAlakasız.
WernerCD

5
Microsoft ekibi, (1) C # 'da doğru olanı yapmak için iyi bir çaba sarf etmek ve (2) daha fazla değil doğru almak için yeterli kredi alamıyor.
codenheim

2
@Voo: Eğer örtük bir dönüşüm uygulamayı seçerseniz boolo zaman kullanabilirsiniz &&ve ||uygulama olmadan operator true/falseveya operator &/|C # sorunsuz. Sorun tam olarak boolmümkün olmayan bir dönüşümün olmadığı veya istenmediği durumlarda ortaya çıkar .
Eric Lippert

43

Mesele şu ki (C ++ 98 sınırları içinde) sağ taraftaki işlenen, argüman olarak aşırı yüklenmiş operatör fonksiyonuna aktarılacaktır. Bunu yaparken zaten değerlendirilmiş olur . Hiçbir şey operator||()ya operator&&()ya bu önleyeceğini yapamazdım olabilir kodu.

Orijinal işleç farklıdır, çünkü bu bir işlev değildir, ancak dilin daha düşük bir düzeyinde uygulanır.

Ek dil özellikleri olabilir sağ elin dışı değerlendirmesini işlenen sözdizimsel yapmış mümkün . Ancak, bunlar anlamsal olarak yararlı olacak sadece birkaç vaka olduğu için rahatsız etmediler . (Tıpkı ? :aşırı yükleme için mevcut olmayan gibi .

(Lambdaları standart haline getirmek 16 yıl sürdü ...)

Anlamsal kullanıma gelince, şunları göz önünde bulundurun:

objectA && objectB

Bu kaynar:

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

Bir dönüşüm operatörünü çağırmak dışında, burada objectB (bilinmeyen tipte) ile tam olarak ne yapmak istediğinizi ve bunu booldil tanımı için kelimelere nasıl koyacağınızı düşünün.

Ve eğer sen edilir de, bool dönüşümü çağıran ...

objectA && obectB

aynı şeyi yapıyor, şimdi yapıyor mu? Peki neden ilk etapta aşırı yük?


7
mantık hatanız, şu anda tanımlanmış olan dilde, farklı tanımlanmış bir dilin etkileri hakkında akıl yürütmektir. eski günlerde birçok yeni başlayanlar bunu yapıyordu. msgstr "sanal kurucu". onları böyle bir kutu düşüncesinden çıkarmak için aşırı miktarda açıklama gerekiyordu. her neyse, yerleşik operatörlerin kısa devre yapmasıyla, tartışmanın değerlendirilmemesi konusunda garantiler vardır. bu garanti, onlar için kısa devre tanımlanmışsa, kullanıcı tanımlı aşırı yükler için de olacaktır.
Şerefe ve s. - Alf

1
@iFreilicht: Temelde Deduplicator veya Piotr ile aynı şeyi söyledim, sadece farklı kelimelerle. Düzenlenen cevaptaki noktayı biraz detaylandırdım. Bu şekilde çok daha elverişliydi, yakın zamana kadar gerekli dil uzantıları (örn. Lambdas) yoktu ve fayda yine de önemsiz olurdu. Sorumlu kişilerin derleyici üreticileri tarafından henüz yapılmayan bir şeyi "sevdiği" birkaç kez , 1998'de geri tepti. (Bkz export.)
DevSolar

9
@iFreilicht: Her booliki sınıf için de bir dönüştürme işleci tüm üye değişkenlere de erişebilir ve yerleşik işleçle sorunsuz çalışır. Başka bir şey ama dönüşüm-to-bool yine kısa devre değerlendirme için semantik anlam ifade etmez! Anlamsal açıdan değil, sentaktik itibaren bu yaklaşım deneyin: Ne elde etmeye çalıştığınız olacağını değil, nasıl bu konuda giderdim.
DevSolar

1
İtiraf etmeliyim ki birini düşünemiyorum. Kısa devrenin var olmasının tek nedeni, Booleans üzerindeki işlemlere zaman kazandırması ve tüm argümanların değerlendirilmesinden önce bir ifadenin sonucunu bilmenizdir. Diğer VE operasyonlar ile, bu durumda değildir ve bu yüzden &ve &&aynı operatör değildir. Bunu anlamama yardımcı olduğun için teşekkürler.
iFreilicht

8
@iFreilicht: Aksine, kısa devre yapmanın amacı , sol tarafın hesaplanması, sağ tarafın ön koşulunun gerçeğini oluşturabilmesidir . if (x != NULL && x->foo)hız için değil, güvenlik için kısa devre gerektirir.
Eric Lippert

26

Bir özellik düşünülmeli, tasarlanmalı, uygulanmalı, belgelenmeli ve gönderilmelidir.

Şimdi düşündük, şimdi neden kolay olabileceğini görelim (ve o zaman yapmak zor). Ayrıca, sınırlı miktarda kaynak olduğunu unutmayın, bu yüzden eklemek başka bir şey doğranmış olabilir (Bunun için ne yapmak istersiniz?).


Teorik olarak, tüm operatörler , C ++ 11'den itibaren (lambdalar tanıtıldığında, 1979'da "sınıflarla C" başladıktan 32 yıl sonra, hala saygın bir 16 " kısa" ek dil özelliği ile kısa devre davranışına izin verebilir. c ++ 98'den sonra):

C ++, gerekli ve izin verilene kadar (ön koşullar karşılandı) değerlendirmeyi önlemek için tembel olarak değerlendirilen - gizli-lambda - bir argümana açıklama eklemek için bir yola ihtiyaç duyacaktır.


Bu teorik özellik neye benzeyecektir (Yeni özelliklerin yaygın olarak kullanılabilir olması gerektiğini unutmayın)?

Ek açıklama lazy işlev bağımsız değişkenine uygulanan işlevi bir işlev bekleyen bir şablon yapar ve derleyiciyi ifadeyi bir işlevciye dönüştürür:

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

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

Örtünün altına benziyordu:

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

Lambda'nın gizli kaldığına ve en fazla bir kez çağrılacağına dikkat edin. Ortak-alt-ekspresyon-eliminasyon şansının azalmasının yanı sıra, bundan dolayı hiçbir performans düşüşü
olmamalıdır .


Uygulama karmaşıklığı ve kavramsal karmaşıklığın yanı sıra (diğer bazı özellikler için bu karmaşıklıkları yeterince kolaylaştırmadıkça her özellik her ikisini de artırır), bir başka önemli düşünceye bakalım: Geriye dönük uyumluluk.

Bu süre dil özelliği herhangi bir kodu kırmasa da, bundan yararlanan herhangi bir API'yi ustaca değiştirir, yani mevcut kütüphanelerde herhangi bir kullanım sessiz bir kırılma değişikliği anlamına gelir.

BTW: Bu özellik, kullanımı kolay olsa da, C # bölme çözümünden kesinlikle daha güçlüdür &&ve ||her biri ayrı tanım için iki işleve sahiptir.


6
@iFreilicht: "X özelliği neden mevcut değil?" aynı cevaba sahiptir: var olmak için özellik düşünülmüş, iyi bir fikir olarak düşünülmüş, tasarlanmış, belirlenmiş, uygulanmış, test edilmiş, belgelenmiş ve son kullanıcıya gönderilmiş olmalıdır. Bunlardan herhangi biri olmadıysa, özellik yok. Bunlardan biri önerilen özelliğinizde olmadı; hangisinin tarihsel bir araştırma sorunu olduğunu bulmak; Bunlardan hangisinin asla yapılmadığını önemsiyorsanız tasarım komitesindeki kişilerle konuşmaya başlayın.
Eric Lippert

1
@EricLippert: Ve bunun nedenine bağlı olarak, uygulanana kadar tekrarlayın: Belki de çok karmaşık olduğu düşünülüyordu ve hiç kimse yeniden değerlendirme yaptığı düşünülmedi. Ya da yeniden değerlendirme, daha önce reddinden farklı reddetme nedenleriyle sona erdi. (btw: Yorumunuzun özeti eklendi)
Tekilleştirici

@Deduplicator İfade şablonlarında tembel anahtar kelime veya lambdas gerekmez.
Sumant

Tarihsel bir yana, orijinal Algol 68 dilinin "yordamsal" bir zorlamaya sahip olduğunu unutmayın (yanı sıra korumasızlaştırma, bu da bağlam işlev türü yerine sonuç türünü gerektirdiğinde parametresiz bir işlevi örtük olarak çağırmak anlamına gelir). Bu, T türünü döndüren "parametresiz fonksiyon T" ( Algol 68'de " proc T" olarak yazılır) değeri gerektiren bir konumda T tipi bir ifadenin, verilen ifadeyi (örtülü lambda) döndüren dolaylı olarak fonksiyon gövdesine dönüştürüleceği anlamına gelir. 1973 dil revizyonunda özellik kaldırıldı (temelden kaldırma).
Marc van Leeuwen

... C ++ için benzer bir yaklaşım, işleçlerin &&"işlev döndüren T işaretçisine" bir tür bağımsız değişken ve T türünde bir bağımsız değişken ifadesinin dolaylı olarak lambda ifadesine dönüştürülmesine olanak tanıyan ek bir dönüşüm kuralı almak istediğini bildirmek olabilir. Sözdizimsel düzeyde yapılması gerektiği için bunun sıradan bir dönüşüm olmadığını unutmayın: çalışma zamanında T türünde bir değerin bir işleve dönüştürülmesi, değerlendirme zaten yapılmış olacağı için işe yaramaz .
Marc van Leeuwen

13

Retrospektif rasyonalizasyon ile, çünkü

  • garantili kısa devre yapabilmek için (yeni sözdizimi getirmeden) operatörlerin Sonuçlardönüştürülebilir gerçek ilk argüman boolve

  • kısa devre gerektiğinde başka şekillerde kolayca ifade edilebilir.


Örneğin, bir sınıf Tilişkilendirilmiş &&ve ||işleçler içeriyorsa,

auto x = a && b || c;

burada a, bve cÇeşidi deyimleri T, kısa devre ile ifade edilebilir

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

veya belki daha açık bir şekilde

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

Görünen artıklık, operatör çağrılarının yan etkilerini korur.


Lambda yeniden yazımı daha ayrıntılı olsa da, daha iyi kapsüllenmesi kişinin bu tür operatörleri tanımlamasına izin verir .

Aşağıdakilerin (hala biraz influensa) standart uyumluluğundan tamamen emin değilim, ancak temiz Visual C ++ 12.0 (2013) ve MinGW g ++ 4.8.2 ile derler:

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

Çıktı:

000 -> !! !! || yanlış
001 -> !! !! || doğru
010 -> !! !! || yanlış
011 -> !! !! || doğru
100 -> !! && !! || yanlış
101 -> !! && !! || doğru
110 -> !! && !! doğru
111 -> !! && !! doğru

Burada her !!bang-bang bir dönüşümü gösterir bool, yani bir argüman değeri kontrolü.

Bir derleyici aynı şeyi kolayca yapabildiğinden ve ek olarak optimize edebildiğinden, bu kanıtlanmış olası bir uygulamadır ve her türlü imkansızlık iddiası genel olarak imkansızlık iddialarıyla aynı kategoriye konulmalıdır, yani genel olarak kilitlemeler.


Kısa devre değişikliklerinizi, özellikle de üçlü olanı, muhtemelen alabileceğiniz kadar seviyorum.
iFreilicht

Kısa bir devreyi kaçırıyorsunuz &&- - gibi bir ek hat olması gerekir if (!a) { return some_false_ish_T(); }- ve ilk merminize: kısa devre, sonuçlara değil boole dönüştürülebilen parametrelerle ilgilidir.
Arne Mertz

@ArneMertz: "Kayıp" hakkındaki yorumunuz anlamsız. bununla ilgili yorum, evet bunun farkındayım. kısa devre yapmak için dönüşüm boolgereklidir .
Şerefe ve s. - Alf

@ Cheersandhth.-Alf eksik hakkındaki yorum, kısa devre yaptığınız ||ancak cevabınızın ilk revizyonu içindi &&. Diğer yorum, ilk madde işaret noktanızda "boole dönüştürülebilir sonuçlarla sınırlı olması gerekir" - "boole dönüştürülebilir parametrelerle sınırlı " ifadesini okuması amaçlanmıştır .
Arne Mertz

@ArneMertz: Tamam, yeniden versiyonlama, üzgünüm yavaş düzenleme yapıyorum. Kısıtlı boololunursa, sınırlandırılması gereken işleç sonucu değildir, çünkü ifadedeki diğer işleçlerin kısa devrelemesini kontrol etmek için dönüştürülmesi gerekir . Gibi, sonucu mantıksal OR kısa devre kontrol etmek için a && bdönüştürülmelidir . boola && b || c
Şerefe ve s. - Alf

5

tl; dr : oldukça düşük maliyetlere (özel sözdizimi gerekli) kıyasla çok düşük talep (özelliği kim kullanacak?) nedeniyle çabaya değmez.

Akla gelen ilk şey, operatörün aşırı yüklenmesi fonksiyonların yazılmasının sadece fantezi bir yoludur, oysa operatörlerin boolean versiyonu ||ve &&buitlin şeyleridir. Bu, derleyicinin kısa devre yapma özgürlüğüne sahip olduğu anlamına gelirken, x = y && zboolean olmayanlarla ifade yve zbenzer bir fonksiyon çağrısına yol açması gerekir X operator&& (Y, Z). Bu y && z, sadece her iki parametrenin de işlevi çağırmadan önce değerlendirilmesi gereken operator&&(y,z)garip bir şekilde adlandırılmış bir işlevin çağrısı olan (kısa devre için uygun kabul edilen herhangi bir şey dahil) yazmak için süslü bir yol anlamına gelir .

Bununla birlikte, işlevin çağrılmasına ve ardından bir yapıcı çağrısına çevrilen işleç &&için olduğu gibi , işleçlerin çevirisinin biraz daha karmaşık hale getirilmesinin mümkün olabileceği iddia edilebilir .newoperator new

Teknik olarak bu sorun olmaz, kısa devreye olanak sağlayan ön koşula özgü bir dil sözdizimi tanımlanması gerekir. Ancak, kısa devrelerin kullanım durumları ile sınırlı olacaktır Yiçin convetible olan X(sadece ilk parametresinden yani hesaplama sonucu), ya da başka aslında kısa devreyi nasıl yapılacağı ek bilgi olmalıydı. Sonuç şu şekilde görünmelidir:

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

Biri nadiren aşırı yüklenmek ister operator||ve operator&&çünkü nadiren yazmanın a && bgerçekte sezgisel olmayan bir bağlamda sezgisel olduğu bir durum vardır . Bildiğim tek istisna, örneğin gömülü DSL'ler için ifade şablonlarıdır. Ve bu birkaç durumdan sadece birkaçı kısa devre değerlendirmesinden yararlanabilir. İfade şablonları genellikle yoktur, çünkü bunlar daha sonra değerlendirilecek ifade ağaçları oluşturmak için kullanılır, bu nedenle her zaman ifadenin her iki tarafına da ihtiyacınız vardır.

Kısacası: ne derleyici yazarlar ne de standart yazarlar, çemberlerden atlamak ve ek hantal sözdizimi tanımlamak ve uygulamak gereğini hissetmediler, çünkü bir milyonda bir kullanıcı tanımlı kısa devre yapmanın iyi olacağını düşünebilir operator&&ve operator||- sadece el başına mantığı yazmaktan daha az çaba olmadığı sonucuna varmak için.


Maliyet gerçekten bu kadar yüksek mi? D programlama dili, lazyargüman olarak verilen ifadeyi dolaylı olarak anonim bir işleve dönüştüren parametreleri bildirmeye izin verir . Bu, çağrılan işleve bu bağımsız değişkeni çağırma seçeneği sunar. Dilde zaten lambdas varsa, gerekli sözdizimi çok küçüktür. ”Sözde kod”: X ve (Aa, tembel B b) {if (koşul (a)) {kısa (a) dönüş; } else {gerçek (a, b ()); }}
BlackJack

@BlackJack bu tembel parametre std::function<B()>belirli bir ek yüke neden olacak bir a kabul edilerek uygulanabilir . Ya da satır içi yapmak istiyorsanız, bunu yapın template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. Ve belki "normal" Bparametresi ile aşırı yükler , böylece arayan hangi sürümü seçeceğine karar verebilir. lazySözdizimi daha uygun olabilir ama belli bir performans tradeoff sahiptir olabilir.
Arne Mertz

1
İle sorunlardan biri std::functionkarşı lazyilk birden çok kez değerlendirilebilir olmasıdır. fooOlarak kullanılan tembel bir parametre foo+foohala sadece bir kez değerlendirilir.
MSalters

"Kısa devrelerin kullanımı, Y'nin X ile uyumlu olduğu durumlar ile sınırlı olacaktır" ... hayır, tek başına Xhesaplanabilen durumlar ile sınırlıdır Y. Çok farklı. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. "Dönüşüm" çok rahat bir kullanım kullanmadığınız sürece.
Mooing Duck

1
@Mümkün olabilirler. Ancak kısa devre yapan bir geleneğin mantığını operator&&elle de yazabilirsiniz . Soru, mümkün olup olmadığı değil, neden kısa ve uygun bir yol olmadığıdır.
Arne Mertz

5

Lambdas tembellik getirmenin tek yolu değildir. Tembel değerlendirme, C ++ 'da İfade Şablonları kullanılarak nispeten basittir . Anahtar kelimeye gerek yoktur lazyve C ++ 98'de uygulanabilir. İfade ağaçları zaten yukarıda bahsedilmektedir. İfade şablonları zayıf (ama zeki) insanın ifade ağaçlarıdır. İşin püf noktası, ifadeyi Exprşablonun özyinelemeli iç içe yerleştirilmiş örneklerinin bir ağacına dönüştürmektir . Ağaç, inşaattan sonra ayrı ayrı değerlendirilir.

Aşağıdaki kod, kısa devre &&ve ||operatörleri Ssağladığı sürece logical_andve logical_orserbest fonksiyonlar ve dönüştürülebilir olduğu sürece sınıf için uygular bool. Kod C ++ 14'te ancak fikir C ++ 98'de de geçerlidir. Canlı örneğe bakın .

#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

Mantıksal operatörlerin kısa devre yapmasına izin verilir, çünkü ilişkili doğruluk tablolarının değerlendirilmesinde bir "optimizasyon" dur. Bu, mantığın kendisinin bir fonksiyonudur ve bu mantık tanımlanmıştır.

Aslında aşırı yüklenmenin &&ve ||kısa devre yapmamanın bir nedeni var mı ?

Özel aşırı yüklenmiş mantıksal işleçler, bu doğruluk tablolarının mantığını takip etmek zorunda değildir .

Fakat aşırı yüklendiğinde neden bu davranışı kaybediyorlar?

Bu nedenle, tüm fonksiyonun normal şekilde değerlendirilmesi gerekir. Derleyici, normal aşırı yüklenmiş bir operatör (veya işlev) olarak işlem görmelidir ve yine de diğer işlevlerde olduğu gibi optimizasyon uygulayabilir.

İnsanlar mantıksal operatörleri çeşitli nedenlerle aşırı yüklerler. Örneğin; belirli bir alanda, insanların alışkın olduğu "normal" mantıksal alanlar olmayan belirli bir anlamı olabilir.


4

Kısa devre, "ve" ve "veya" doğruluk tablosundan kaynaklanmaktadır. Kullanıcının hangi işlemi tanımlayacağını ve ikinci operatörü değerlendirmek zorunda kalmayacağınızı nasıl bilebilirsiniz?


Yorumlarda ve @Deduplicators cevabında belirtildiği gibi, ek bir dil özelliği ile mümkün olacaktır. Artık işe yaramadığını biliyorum. Benim sorum, böyle bir özellik olmamasının ardındaki mantığın ne olduğuydu.
iFreilicht

Kullanıcının tanımını tahmin etmek zorunda olduğumuzu düşünürsek, kesinlikle karmaşık bir özellik olurdu!
nj-ath

: (<condition>)Operatörün beyanından sonra, ikinci argümanın değerlendirilmediği bir koşulu belirtmeye ne dersiniz ?
iFreilicht

@ iFreilicht: Alternatif bir tekli işlev gövdesine ihtiyacınız var.
MSalters

3

ancak bool operatörleri bu davranışa sahiptir, neden bu tek tiple sınırlandırılmalıdır?

Sadece bu kısma cevap vermek istiyorum. Bunun nedeni, yerleşik &&ve ||ifadelerin aşırı yüklenmiş operatörler gibi işlevlerle uygulanmamasıdır.

Kısa devre mantığının derleyicinin belirli ifadeleri anlaması için yerleşik olması kolaydır. Tıpkı diğer yerleşik kontrol akışları gibi.

Ancak operatör aşırı yüklemesi, belirli kurallara sahip işlevlerle yerine getirilir, bunlardan biri, işlev olarak çağrılmadan önce bağımsız değişken olarak kullanılan tüm ifadelerin değerlendirilmesidir. Açıkçası farklı kurallar tanımlanabilir, ancak bu daha büyük bir iştir.


1
Aşırı yüklenip yüklenmediği sorusuna herhangi bir dikkat edilip edilmediğini merak ediyorum &&,|| ve ,izin verilmelidir? C ++ 'nın aşırı yüklerin işlev çağrıları dışındaki herhangi bir şey gibi davranmasına izin verecek bir mekanizması bulunmaması, bu işlevlerin aşırı yüklerinin neden başka bir şey yapamadığını açıklar, ancak bu operatörlerin neden ilk olarak yüklenebilir olduğunu açıklamaz. Asıl nedenin basitçe çok fazla düşünmeden bir operatör listesine atılmış olmalarıdır.
supercat
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.