Bu süreden kaçınmanın daha iyi yolları nelerdir (0); C ++ hack?


233

Kod akışı şöyle olduğunda:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

Genellikle yukarıdaki dağınık kod akışını önlemek için etrafında bu çalışma gördük:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Daha yüksek düzeyli (endüstri düzeyi) bir kod haline gelmesi için bu geçici çözümden / saldırıdan kaçınmanın daha iyi yolları nelerdir?

Kutunun dışında herhangi bir öneri bekliyoruz!


38
RAII ve atış istisnaları.
ta.speot.is

135
Bana göre, bu iyi bir zaman gibi görünüyor goto- ama eminim birisi bunu önerdiğim için beni işaretleyecektir, bu yüzden bu etkiye bir cevap yazmıyorum. UZUN bir do ... while(0);şey olması yanlış bir şey gibi görünüyor.
Mats Petersson

42
@dasblinkenlight: Evet, gerçekten. Eğer kullanacaksanız goto, bu konuda dürüst olun ve açıkta yapın, breakve bunu kullanarak saklamayın vedo ... while
Mats Petersson

44
@MatsPetersson: Cevap ver, gotorehabilitasyon çaban için sana +1 vereceğim . :)
wilx

27
@ ta.speot.is: "RAII ve atış istisnaları". Bunu yaparak, akış kontrolünü istisnalarla taklit edeceksiniz. Yani bir çekiç ya da bir kağıt ağırlığı olarak pahalı, kanayan kenar donanımı kullanmak gibi. Bunu yapabilirsiniz, ama bu kesinlikle benim için çok kötü bir tat gibi görünüyor.
SigTerm

Yanıtlar:


309

Bu kararları bir işlevde izole etmek ve returns yerine breaks kullanmak kabul edilebilir bir uygulama olarak kabul edilir . Tüm bu kontroller işlevle aynı soyutlama seviyesine karşılık gelse de, oldukça mantıklı bir yaklaşımdır.

Örneğin:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}

22
@MatsPetersson: "İşlevde ayır", yalnızca testleri yapan yeni bir işleve yeniden düzenleme anlamına gelir.
MSalters

35
+1. Bu da iyi bir cevap. C ++ 11'de, yerel değişkenleri de yakalayabileceği için yalıtılmış fonksiyon bir lambda olabilir, bu yüzden işleri kolaylaştırır!
Nawaz

4
@deworde: Duruma bağlı olarak bu çözüm goto'dan çok daha uzun ve daha az okunabilir hale gelebilir. C ++ maalesef yerel işlev tanımlarına izin vermediğinden, bu işlevi (geri döneceğiniz) başka bir yere taşımanız gerekir, bu da okunabilirliği azaltır. Düzinelerce parametre ile sonuçlanabilir, çünkü düzinelerce parametreye sahip olmak kötü bir şey olduğundan, bunları yeni veri türü oluşturacak olan yapıya sarmaya karar vereceksiniz. Basit bir durum için çok fazla yazı yazmak.
SigTerm

24
@Damon bir şey varsa, returndaha temiz çünkü herhangi bir okuyucu hemen doğru çalıştığını ve ne yaptığını biliyor. İle gotobunun için ne olduğunu görmek için ve emin hiçbir hata yapıldığını olmak etrafına bakmak gerekiyor. Disguising olduğu fayda.
R. Martinho Fernandes

11
@SigTerm: Bazen her işlevin boyutunu, kolay anlaşılır bir boyuta getirmek için kod ayrı bir işleve taşınmalıdır. Bir işlev çağrısı için ödeme yapmak istemiyorsanız, işaretleyin forceinline.
Ben Voigt

257

Kullanmanın gotogerçekte DOĞRU cevap olduğu zamanlar vardır - en azından dini inanca yetişmeyenlere "goto sorunun ne olursa olsun, asla cevap olamayacağı - ve bu da bu davalardan biridir.

Bu kod do { ... } while(0);sadece gotoa break. Eğer kullanacaksangoto , o zaman açık olun. HARDER kodunun okunmasını sağlamanın bir anlamı yok.

Özel bir durum, oldukça karmaşık koşullara sahip çok fazla kodunuz olduğunda:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

Aslında böyle karmaşık bir mantığa sahip olmadan kodun doğru olduğunu TEMİZLE yapabilir.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Düzenleme: Açıklığa kavuşturmak için, hiçbir şekilde goto genel bir çözüm olarak . Ancak,goto diğer çözümlerden daha iyi bir çözümün olduğu .

Örneğin, bazı veriler topladığımızı ve test edilmekte olan farklı koşulların "toplanan verilerin sonu olduğu" bir çeşit olduğunu düşünün. veri akışındasınız.

Şimdi, işimiz bittiğinde, verileri bir dosyaya kaydetmemiz gerekiyor.

Ve evet, genellikle makul bir çözüm sağlayabilen, ancak her zaman değil başka çözümler de vardır.


76
Katılmıyorum. gotobir yeri olabilir ama goto cleanupyoktur. Temizleme RAII ile yapılır.
MSalters

14
@ MSalters: Bu, temizlemenin RAII ile çözülebilecek bir şey içerdiğini varsayar. Belki onun yerine "sorun hatası" ya da böyle bir şey söylemeliydim.
Mats Petersson

25
Bu nedenle, muhtemelen dini inancın inancından gotoasla aşağı doğru cevap almamaya devam edin . Yorum varsa takdir ediyorum ...
Mats Petersson

19
insanlar gotobunu kullanmayı düşünmek zorunda / bunu kullanan bir programı anlamak zorundasınız ... Öte yandan, mikroişlemciler üzerine inşa edilmiş jumpsve conditional jumps... sorun mantık ya da başka bir şey ile değil, bazı insanlarda.
woliveirajr

17
Uygun kullanımı için +1 goto. RAII olduğunu değil bu istisnalar kötüye olacağı gibi, hatalar bekleniyorsa doğru çözüm (olağanüstü).
Joe

82

Bir booldeğişkenle basit bir devam düzeni kullanabilirsiniz :

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Bu yürütme zinciri checkNgeri döner dönmez duracaktır a false. Operatörün check...()kısa devresi nedeniyle başka arama yapılmaz &&. Üstelik, optimize derleyiciler bu ayarı tanımak için akıllı yeterli goOnüzere falsetek yönlü bir sokak olduğunu ve eksik eklemek goto endsenin için. Sonuç olarak, yukarıdaki kodun performansı yalnızca okunabilirliğine acı verici bir darbe olmadan a do/ ile aynı olacaktır while(0).


30
Koşullar içindeki ifgörevlendirmeler çok şüpheli görünüyor .
Mikhail

90
@Mikhail Sadece eğitimsiz bir göze.
dasblinkenlight

20
Daha önce bu tekniği kullandım ve her zaman tür bir derleyici, erken bir başarısız olsa bile (atlama / ayrılmanın aksine) ifne olursa olsun her birini kontrol etmek için kod üretmek zorunda olduğunu düşündüm goOn. ama sadece bir test yaptım ve VS2012 en azından ilk yanlıştan sonra her şeyi kısa devre yapacak kadar akıllıydı. Bunu daha sık kullanacağım. Not: Eğer kullanırsanız goOn &= checkN()o zaman checkN()her zaman bile çalışacak goOnoldu falsebaşlangıcında if(yani bunu yapma).
mark

11
@Nawaz: Kod tabanı üzerinde keyfi değişiklikler yapan eğitimsiz bir zihniniz varsa, ifs içindeki atamalardan çok daha büyük bir sorununuz var demektir .
Idelic

6
@sisharp Elegance seyircinin gözünde! Dünyada bir döngü yapısının yanlış kullanımının nasıl "zarif" olarak algılanabileceğini anlayamıyorum, ama belki de bu sadece benim.
dasblinkenlight

38
  1. Kodu ayrı bir işleve (veya belki birden fazla) ayıklamaya çalışın. Ardından, kontrol başarısız olursa işlevden geri dönün.

  2. Bunu yapmak için çevredeki kodla çok sıkı bir şekilde bağlanmışsa ve kaplini azaltmanın bir yolunu bulamıyorsanız, bu bloktan sonraki koda bakın. Muhtemelen, fonksiyon tarafından kullanılan bazı kaynakları temizler. Bir RAII nesnesi kullanarak bu kaynakları yönetmeye çalışın ; daha sonra her tehlikeli değiştirin breakile return(ya da throw, o daha uygun ise) ve sizin için nesnenin yıkıcı temiz yukarı edelim.

  3. Program akışı (zorunlu olarak) gerçekten ihtiyacınız olan kadar kıvrımlıysa, gotogarip bir kılık vermek yerine bunu kullanın.

  4. Körü körüne yasaklayan kodlama kurallarınız varsa gotove program akışını gerçekten basitleştiremiyorsanız, muhtemelen bunu korsanınızla gizlemeniz gerekir do.


4
Alçakgönüllülükle RAII'nin yararlı olsa da gümüş bir kurşun olmadığını söylüyorum. Kendinizi başka bir kullanımı olmayan bir convert-goto-RAII sınıfı yazmak üzere bulduğunuzda, kesinlikle daha önce bahsettiğim "dünyanın sonu" deyimini kullanarak daha iyi hizmet edeceğinizi düşünüyorum.
idoby

1
@busy_wait: Gerçekten, RAII her şeyi çözemez; bu yüzden cevabım ikinci nokta ile bitmiyor, ama gotobunun gerçekten daha iyi bir seçenek olup olmadığını önermeye devam ediyor .
Mike Seymour

3
Kabul ediyorum, ancak goto-RAII dönüşüm sınıfları yazmak kötü bir fikir ve açık bir şekilde ifade edilmesi gerektiğini düşünüyorum.
idoby

@idoby bir RAII şablon sınıfı yazma ve bir
lambda'da

@Caleth, ktorları / dtorları yeniden keşfediyormuşsun gibi geliyor mu?
idoby

37

TLDR : RAII , işlem kodu (yalnızca önceden hesaplandığında sonuçları ayarlayın veya bir şeyler ) ve istisnalar.

Uzun cevap:

In C , kod bu tür iyi uygulama bir EXIT / CLEANUP / eklemektir diğer yerel kaynakların temizleme olur ve bir hata kodu (varsa) döndürülür nerede, kod etiketi. Bu en iyi yöntemdir çünkü kodu doğal olarak başlatma, hesaplama, taahhüt ve geri dönüşe böler:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

C'de, çoğu kod tabanında, if(error_ok != ...ve gotokodu genellikle bazı kullanışlılık makrolarının arkasında gizlenir ( RET(computation_result),ENSURE_SUCCESS(computation_result, return_code) vs.).

C ++ , C üzerinden ekstra araçlar sunar :

  • Temizleme bloğu işlevselliği RAII olarak uygulanabilir, yani artık tüm cleanupbloğa ihtiyacınız yoktur ve istemci kodunun erken dönüş ifadeleri eklemesini sağlar.

  • Devam edemediğiniz zaman atarsınız, if(error_ok != ...hepsini basit aramalara dönüştürürsünüz.

Eşdeğer C ++ kodu:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

Bu en iyi uygulamadır çünkü:

  • Açıktır (yani, hata işleme açık olmasa da, algoritmanın ana akışı)

  • İstemci kodu yazmak basittir

  • Minimal

  • Basit

  • Tekrarlayan kod yapıları yoktur

  • Makro kullanmaz

  • Tuhaf do { ... } while(0)yapılar kullanmaz

  • Çok az çaba ile tekrar kullanılabilir (yani, çağrıyı computation2();farklı bir işleve kopyalamak istiyorsam do { ... } while(0), yeni bir kodda #definebir goto sarmalayıcı makro ve bir temizleme etiketi veya başka herhangi bir şey).


+1. RAII veya başka bir şey kullanarak kullanmaya çalıştığım şey bu. shared_ptrözel bir deleter ile bir çok şey yapabilirsiniz. C ++ 11'deki lambdaslarla daha da kolay.
Ağustos'ta Macke

Doğru, ancak işaretçi olmayan şeyler için shared_ptr kullanırsanız, en azından şunu tanımlamayı düşünün: namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };Bu durumda ( make_handleyapı üzerinde doğru silme türünü ayarlayarak), türün adı artık bir işaretçi olduğunu göstermez .
utnapistim

21

Bütünlük uğruna bir cevap ekliyorum. Diğer bazı cevaplar, büyük durum bloğunun ayrı bir fonksiyona ayrılabileceğine dikkat çekti. Ancak birkaç kez de belirtildiği gibi, bu yaklaşımın koşullu kodu orijinal bağlamdan ayırmasıdır. C ++ 11'de lambdaların dile eklenmesinin bir nedeni budur. Lambdas kullanımı başkaları tarafından önerildi, ancak açık bir örnek verilmedi. Bu cevaba bir tane koydum. Bana çarpıcı gelen do { } while(0), birçok yönden yaklaşıma çok benzer hissetmesidir - ve belki de bu hala gotokılık değiştirmiş demektir ...

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations

7
Bana göre bu kesmek yapmaktan daha kötü görünüyor ...
Michael

18

Kesinlikle cevap değil , bir cevap (bütünlük uğruna)

Onun yerine :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Yazabilirsiniz:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Bu hala bir olduğunu Goto maskeli, ama en azından bir döngü artık değil. Bu, devam etmek için çok dikkatli bir şekilde kontrol etmeniz gerekmeyeceği anlamına gelir , blokta bir yerlerde gizli etmenin .

Yapı, derleyicinin onu optimize edeceğini umabileceğiniz kadar basittir.

@Jamesdlin tarafından önerildiği gibi, bunu gibi bir makronun arkasına gizleyebilirsiniz.

#define BLOC switch(0) case 0:

Ve şu şekilde kullanın

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Bu mümkündür, çünkü C dili sözdizimi köşeli parantez içindeki bir bloktan değil, bir anahtardan sonra bir ifade bekler ve bu ifadeden önce bir vaka etiketi koyabilirsiniz. Şimdiye kadar buna izin verme noktasını görmedim, ancak bu özel durumda anahtarı güzel bir makronun arkasına gizlemek kullanışlı.


2
Ooh, akıllı. Hatta bunu gibi bir makronun arkasına saklayabilir define BLOCK switch (0) case 0:ve kullanabilirsiniz BLOCK { ... break; }.
jamesdlin

@jamesdlin: bir anahtarın açılış braketinden önce bir dava koymak yararlı olabileceğinden daha önce hiç aklıma gelmedi. Ancak C tarafından gerçekten izin verilir ve bu durumda güzel bir makro yazmak yararlıdır.
kriss

15

Gereksiz Mats cevap benzer bir yaklaşım öneriyoruz goto. Sadece koşullu mantığı işleve koy. Her zaman çalışan herhangi bir kod, işlev çağıranda çağrılmadan önce veya sonra gitmelidir:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}

3
Ortasında başka bir kaynak funcedinmeniz gerekiyorsa (deseninize göre) başka bir işlevi çarpanlarına ayırmanız gerekir. Tüm bu yalıtılmış işlevlerin aynı verilere ihtiyacı varsa, aynı yığın argümanlarını tekrar tekrar kopyalamanız ya da argümanlarınızı öbek üzerinde tahsis etmeye ve dilin en temel özelliklerinden yararlanmadan bir işaretçi aktarmaya karar vermeniz yeterlidir ( işlev bağımsız değişkenleri). Kısacası, bu çözümün, temizlenmesi gereken yeni kaynaklar elde ettikten sonra her koşulun kontrol edildiği en kötü durum için ölçeklendiğine inanmıyorum. Not Nawaz lambdas hakkında yorum yapar.
tne

2
OP'nin kod bloğunun ortasında kaynak elde etme hakkında bir şey söylediğini hatırlamıyorum, ancak bunu mümkün bir gereklilik olarak kabul ediyorum. Bu durumda, yığındaki kaynağı herhangi bir yerde bildirmek func()ve yıkıcısının serbest kaynakları işlemesine izin vermekte sorun nedir? Dışarıdaki herhangi bir şeyin func()aynı kaynağa erişmesi gerekiyorsa, func()uygun bir kaynak yöneticisi tarafından çağrılmadan önce öbek üzerinde bildirilmelidir .
Dan Bechard

12

Kod akışının kendisi zaten fonksiyonda çok fazla olan bir kod kokusudur. Buna doğrudan bir çözüm yoksa (işlev genel bir kontrol işlevidir), RAII kullanmak, böylece işlevin bir uç bölümüne atlamak yerine geri dönebilirsiniz, daha iyi olabilir.


11

Yürütme sırasında yerel değişkenleri tanıtmanız gerekmiyorsa, bunu genellikle düzleştirebilirsiniz:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()

2
Ancak daha sonra her koşul, sadece çirkin değil, aynı zamanda performans açısından kötü olan bir öncekini de içermelidir. "Tamam" olduğumuzu öğrenir öğrenmez bu kontrolleri atlayıp temizlememiz daha iyi olur.
tne

Bu doğrudur, ancak sonunda yapılması gereken şeyler varsa bu yaklaşımın avantajları vardır, bu nedenle erken dönmek istemezsiniz (düzenlemeye bakın). Denise'in koşullarda bools kullanma yaklaşımıyla birleştirirseniz, çok sıkı bir döngüde olmadıkça performans isabeti ihmal edilebilir.
the_mandrill

10

Dasblinkenlight'ın cevabına benzer, ancak içindeki ifkod ataması tarafından "düzeltilebilecek" atamayı önler :

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

Bir adımın sonuçlarının bir sonraki adımdan önce kontrol edilmesi gerektiğinde, tüm kontrollerin büyük if( check1() && check2()...tipte bir desenle yapılabileceği bir durumdan farklı olduğunda bu kalıbı kullanırım .


Check1 () işlevinin gerçekten adımın sonuç kodunu döndüren PerformStep1 () olabileceğini unutmayın. Bu, işlem akışı işlevinizin karmaşıklığını azaltacaktır.
Denise Skidmore

10

İstisnalar kullanın. Kodunuz çok daha temiz görünecek (ve bir programın yürütme akışındaki hataları işlemek için istisnalar oluşturuldu). Kaynakları temizlemek için (dosya tanımlayıcıları, veritabanı bağlantıları, vb.), C ++ neden bir "nihayet" yapı sağlamıyor? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}

10
Gerçekten mi? İlk olarak, dürüstçe, herhangi bir okunabilirlik artışı görmüyorum. PROGRAM AKIŞINI KONTROL ETMEK İÇİN İSTİSNALARI KULLANMAYIN. Bu onların doğru amacı DEĞİLDİR. Ayrıca, istisnalar önemli performans cezaları getirir. Bir istisna için uygun kullanım durumu, başka bir işlem tarafından zaten kilitli olan bir dosyayı açmaya çalışmak veya bir ağ bağlantısı başarısız olduğunda veya bir veritabanına çağrı başarısız olduğunda gibi , veya arayan bir yordama geçersiz bir parametre iletir. Bu tür bir şey. Ancak program akışını kontrol etmek için istisnalar KULLANMAYIN.
Craig

3
Yani, bu konuda bir sümük olmamak, ama referans verdiğiniz Stroustrup makalesine gidin ve "Ne için istisnaları kullanmamalıyım?" Bölüm. Diğer şeylerin yanı sıra şöyle diyor: "Özellikle atma, bir işlevden (döndürmeye benzer) bir değer döndürmenin alternatif bir yolu değildir. Bunu yapmak yavaş olur ve yalnızca hata için kullanılan istisnaları görmek için kullanılan çoğu C ++ programcısını karıştırır. Benzer şekilde, atış bir döngüden kurtulmanın iyi bir yolu değildir. "
Craig

3
@Craig İşaret ettiğiniz her şey doğrudur, ancak bir check()koşul başarısız olduktan sonra örnek programın devam edebileceğini ve bu durumun SİZİN varsayımınız olduğunu kabul edersiniz, örnekte bağlam yoktur. Programın devam edemeyeceği varsayılarak, istisnalar kullanmak bir yoldur.
Cartucho

2
Aslında bu bağlamsa yeterince doğrudur. Ancak, varsayımlar hakkında komik bir şey ... ;-)
Craig

10

Benim do{...}while(0)için sorun değil. Öğesini görmek istemiyorsanız do{...}while(0), onlar için alternatif anahtar kelimeler tanımlayabilirsiniz.

Misal:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST

Bence derleyici gereksiz while(0)koşulu kaldıracakdo{...}while(0) ikili sürümünde ve sonları koşulsuz atlama dönüştürecek düşünüyorum. Emin olmak için montaj dili sürümünü kontrol edebilirsiniz.

Kullanmak gotoda daha temiz kod üretir ve koşulu atla mantığıyla anlaşılır. Aşağıdakileri yapabilirsiniz:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.

Etiketin kapanıştan sonra yerleştirildiğini unutmayın }. Bu, gotoetiketi göremediğiniz için yanlışlıkla bir kod yerleştirmekten olası bir sorundan kaçınmaktır . Artık do{...}while(0)durum kodu olmadan olduğu gibi .

Bu kodu daha temiz ve daha anlaşılır hale getirmek için şunları yapabilirsiniz:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

Bununla, iç içe bloklar yapabilir ve nereden çıkmak / atlamak istediğinizi belirleyebilirsiniz.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

Spagetti kodu hata değildir goto, programcının hatasıdır. Kullanmadan yine de spagetti kodu üretebilirsiniz goto.


10
gotoÖn işlemciyi milyon kez kullanarak dil sözdizimini genişletmeyi tercih ederim .
Hıristiyan

2
"Bu kodu daha temiz ve daha anlaşılır hale getirmek için [LOADS_OF_WEIRD_MACROS kullanabilirsiniz]" : hesaplanmaz.
underscore_d

8

Bu, işlevsel bir programlama perspektifinden (belki de monad) iyi bilinen ve iyi çözülmüş bir sorundur.

Ama aşağıdan alınan yorumuna cevaben burada açılımımı düzenledikten: Sen uygulamayla ilgili tüm ayrıntıları bulabilirsiniz C ++ içinde monads çeşitli yerlerde sen Rotsor önerdiklerini elde sağlayacaktır. Monad'ları grok yapmak biraz zaman alıyor, bunun yerine burada hızlı bir "fakir-mans" monad benzeri bir mekanizma önereceğim.

Hesaplama adımlarınızı aşağıdaki gibi ayarlayın:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Her bir hesaplama adımı boost::none, verilen isteğe bağlı boşsa, geri dönüş gibi bir şey yapabilir . Yani mesela:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Sonra onları birbirine zincirleyin:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

Bununla ilgili güzel olan şey, her bir hesaplama adımı için açıkça tanımlanmış birim testleri yazabilmenizdir. Ayrıca çağırma düz İngilizce gibi okunur (genellikle işlevsel stilde olduğu gibi).

Değişmezliği umursamıyorsanız ve paylaşımlı_ptr veya benzerlerini kullanarak her değişiklik yaptığınızda aynı nesneyi geri döndürmek daha uygunsa.


3
Bu kod, ayrı ayrı işlevlerin her birini bir önceki işlevin başarısızlığını işlemeye zorlama, dolayısıyla Monad deyimini (bu durumda başarısızlık durumunda, monadik etkilerin dolaylı olarak ele alınması gerektiği) doğru şekilde kullanmayan bir istenmeyen özelliğe sahiptir. Bunun için optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);fonksiyon uygulamasından ziyade monadik kompozisyon işlemini ('bind') kullanmanız gerekir.
Rotsor

Teşekkürler. Haklısın - düzgün yapmanın yolu bu. Cevabımı açıklamak için çok fazla yazmak istemedim (bu nedenle burada bütün domuz gitmediğimi ima etmek anlamına gelen "yoksul mans" terimi).
Benedict

7

İf ifadelerini sayısal veya enum sonucu veren ekstra bir işleve taşımaya ne dersiniz?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}

Bu mümkün olduğunda güzeldir, ancak burada sorulan sorudan çok daha az geneldir. Her koşul, son debranching testinden sonra yürütülen koda bağlı olabilir.
kriss

Buradaki sorun, ayrı bir neden ve sonuçtur. Yani aynı koşul numarasına atıfta bulunan ayrı bir koddur ve bu başka hataların kaynağı olabilir.
Riga

@kriss: ConditionCode () işlevi bununla ilgilenecek şekilde ayarlanabilir. Bu işlevin kilit noktası, son koşul hesaplanır hesaplanmaz temiz bir çıkış için <result> dönüşünü kullanabilmenizdir; Burada yapısal netlik sağlayan da budur.
karx11erx

@Riga: Imo bunlar tamamen akademik itirazlar. Her yeni sürümde C ++ 'ın daha karmaşık, şifreli ve okunamaz hale geldiğini düşünüyorum. Hedef fonksiyonu daha okunaklı hale getirmek için karmaşık koşulları iyi yapılandırılmış bir şekilde değerlendiren küçük bir yardımcı işlev ile ilgili bir sorun göremiyorum.
karx11erx

1
@ karx11erx itirazlarım pratik ve deneyimlerime dayanıyor. Bu model, C ++ 11 veya herhangi bir dil ile ilgisizdir. Bir dil sorunu olmayan iyi mimari yazmanıza izin veren dil yapılarıyla ilgili zorluklarınız varsa.
Riga

5

Belki böyle bir şey

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

veya istisnalar kullanın

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

İstisnalar kullanarak veri de iletebilirsiniz.


10
Lütfen şimdiye kadarki tanımınız gibi akıllıca şeyler yapmayın, genellikle diğer geliştiriciler için kodu okumayı zorlaştırırlar. Birinin Case'i break olarak tanımladığını gördüm; bir başlık dosyasında vaka ve bir cpp dosyasındaki bir anahtarda kullandım, diğerleri neden Case ifadeleri arasında geçişin neden kırıldığını merak ettiler. Grrr ...
Michael

5
Ve makroları adlandırdığınızda, makrolar gibi görünmelerini sağlamalısınız (yani, tümüyle büyük harf). Aksi takdirde, değişken / işlev / tür / vb. Olarak adlandırılan biri. adlı everçok mutsuz olacak ...
jamesdlin

5

Yol kullanmıyorum özellikle içine breakveyareturn böyle bir durumda da böyle bir durumda . Normalde böyle bir durumla karşı karşıya kaldığımızda, genellikle nispeten uzun bir yöntemdir.

Birden fazla çıkış noktamız varsa, belirli mantığın yürütülmesine neyin neden olacağını bilmek istediğimizde zorluklara neden olabilir: Normalde sadece bu mantığı çevreleyen blokları yukarı kaldırmaya devam ederiz ve bu bloğun kriterleri bize durum:

Örneğin,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

Kapalı bloklara bakarak, myLogic()sadece conditionA and conditionB and conditionCdoğru olduğunda olduğunu bulmak kolaydır .

Erken iade olduğunda çok daha az görünür hale gelir:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

myLogic()Durumu anlamak için kapalı bloğa bakarak artık yukarı gidemeyiz.

Kullandığım farklı geçici çözümler var. İşte bunlardan biri:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(Elbette hepsini değiştirmek için aynı değişkeni kullanmak memnuniyetle karşılanır. isA isB isC .)

Böyle bir yaklaşım, en az bir kod okuyucu verecek myLogic()yürütülür isB && conditionC. Okuyucuya isB'nin gerçek olmasına neden olacak şeyi daha fazla araması gerektiği konusunda bir ipucu verilir.


3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

Peki ya bu?


if eski, sadece return condition;, aksi takdirde bu iyi muhafaza edilebilir olduğunu düşünüyorum.
SpaceTrucker

2

Hatanın nerede olduğuna bağlı olarak farklı temizleme adımlarına ihtiyacınız varsa yararlı olan başka bir desen:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }

2

Ben bir C ++ programcısı değilim , bu yüzden burada herhangi bir kod yazmayacağım, ama şimdiye kadar kimse nesne odaklı bir çözüm bahsetmedi. İşte benim tahminim:

Tek bir koşulu değerlendirmek için bir yöntem sağlayan genel bir arayüze sahip olun. Şimdi, söz konusu yöntemi içeren nesnenizdeki bu koşulların uygulamalarının bir listesini kullanabilirsiniz. Listeyi tekrarlar ve her bir koşulu değerlendirirsiniz, muhtemelen başarısız olursa erken patlarsınız.

İyi olan, bu tasarımın açık / kapalı prensibine çok iyi yapışmasıdır , çünkü söz konusu yöntemi içeren nesnenin başlatılması sırasında kolayca yeni koşullar ekleyebilirsiniz. Koşul değerlendirmesi için koşulun açıklamasını döndüren yöntemle arabirime ikinci bir yöntem bile ekleyebilirsiniz. Bu, kendi kendini belgeleyen sistemler için kullanılabilir.

Bununla birlikte, dezavantajı, daha fazla nesnenin kullanılması ve liste üzerindeki yineleme nedeniyle biraz daha fazla ek yükün olmasıdır.


Farklı bir dilde örnek ekleyebilir misiniz? Özellikle C ++ hakkında sorulmuş olmasına rağmen bu soru birçok dil için geçerli olduğunu düşünüyorum.
Denise Skidmore

1

Ben böyle yapıyorum.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}

1

İlk olarak, neden gotoC ++ için iyi bir çözüm olmadığını gösteren kısa bir örnek :

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

Bunu bir nesne dosyasına derlemeye çalışın ve ne olduğunu görün. Sonra eşdeğer do+ break+ ' yı deneyin while(0).

Bu bir yana. Ana nokta bunu takip ediyor.

Bu küçük kod parçaları , tüm işlev başarısız olursa genellikle bir tür temizleme gerektirir . Kısmen bitmiş hesaplamayı "gevşettiğiniz" için, bu temizlemeler genellikle parçaların kendilerinin tersi sırada olmak ister.

Bu semantiği elde etmenin bir yolu RAII'dir ; @ utnapistim'in cevabına bakınız. C ++, otomatik yıkıcıların doğal olarak bir "gevşetme" sağlayan inşaatçılara ters sırada çalışmasını garanti eder.

Ancak bu birçok RAII sınıfı gerektirir. Bazen daha basit bir seçenek sadece yığını kullanmaktır:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...ve bunun gibi. "Geri al" kodunu "do" kodunun yanına koyduğundan, denetlemesi kolaydır. Kolay denetim iyidir. Ayrıca kontrol akışını çok netleştirir. C için de faydalı bir modeldir.

calcİşlevlerin çok sayıda argüman almasını gerektirebilir , ancak sınıflarınız / yapılarınız iyi bir uyum içerdiğinde bu genellikle bir sorun değildir. (Yani, birbirine ait olan şeyler tek bir nesnede yaşar, bu nedenle bu işlevler az sayıda nesneye işaret veya referans alabilir ve yine de birçok yararlı iş yapabilir.)


Temizleme yolunu denetlemek çok kolay, ama belki de altın yolu izlemek o kadar kolay değil. Ama genel olarak, böyle bir şeyin tutarlı bir temizleme modelini teşvik ettiğini düşünüyorum.
Denise Skidmore

0

Kodunuzda if..else if..else ifadelerinin uzun bir bloğu varsa, tüm bloğu Functorsveya yardımıyla yeniden yazmayı deneyebilirsiniz function pointers. Her zaman doğru çözüm olmayabilir, ancak oldukça sıktır.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html


Prensipte bu mümkündür (ve kötü değil), ancak açık fonksiyon nesneleri veya -puanerler ile kod akışını çok fazla bozar. OTOH, burada eşdeğer, lambdaların veya sıradan isimlendirilmiş fonksiyonların kullanımı iyi bir uygulamadır, verimli ve iyi okur.
leftaroundabout

0

Burada sunulan farklı cevapların sayısına hayran kaldım. Ama nihayet değiştirmek zorunda olduğum kodda (yani bu do-while(0)hack veya herhangi bir şeyi kaldırmak ), burada belirtilen cevapların herhangi birinden farklı bir şey yaptım ve neden kimse bunu düşündüğünü kafam karıştı. İşte yaptım:

Başlangıç ​​kodu:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Şimdi:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

Burada yapılan, bitirme işlerinin bir fonksiyonda izole edilmiş olması ve işlerin aniden çok basit ve temiz hale gelmesidir!

Bu çözümün burada bahsetmeye değer olduğunu düşündüm.


0

Tek bir ififadede birleştirin:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

Bu, goto anahtar kelimesinin kaldırıldığı Java gibi dillerde kullanılan modeldir.


2
Bu sadece durum testleri arasında herhangi bir şey yapmanız gerekmiyorsa işe yarar. (şey, sanırım durum testleri tarafından yapılan bazı işlev çağrılarının içinde işlerinizi gizleyebilirsiniz, ancak çok fazla yaparsanız bu biraz şaşırtabilir)
Jeremy Friesner

@JeremyFriesner Aslında aradaki şeyleri her zaman olarak değerlendirilen ayrı boole işlevleri olarak yapabilirsiniz true. Kısa devre değerlendirmesi, tüm ön koşul testlerinin geçmediği şeyler arasında asla çalışmamanızı garanti eder.
AJMansfield

@AJMansfield evet, ikinci cümlemde bahsettiğim şey bu ... ama bunu yapmak kod kalitesinde bir gelişme olacağını bilmiyorum.
Jeremy Friesner

@JeremyFriesner Hiçbir şey, koşulları (/*do other stuff*/, /*next condition*/)güzel bir şekilde biçimlendirebileceğiniz gibi yazmanızı engelleyemez . İnsanların hoşuna gitmesini beklemeyin. Ama dürüst olmak gerekirse, bu sadece o dışarı faza Java için bir hata olduğunu göstermek için gider goto... deyimi
cmaster - Yeniden aktifleştirme Monica

@JeremyFriesner Bunların boolean olduğunu varsayıyordum. Eğer fonksiyonlar her koşulun içinde çalıştırılacaksa, onu idare etmenin daha iyi bir yolu vardır.
Tyzoid

0

Tüm hatalar için aynı hata işleyiciyi kullanıyorsanız ve her adımda başarıyı gösteren bir bool döndürülür:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(Tyzoid'in cevabına benzer, ancak koşullar eylemlerdir ve && ilk başarısızlıktan sonra ek eylemlerin gerçekleşmesini önler.)


0

Neden işaretleme yöntemi cevaplanmadığı çağlardan beri kullanılmaktadır.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
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.