อะไรคือวิธีที่ดีกว่าในการหลีกเลี่ยงสิ่งที่ต้องทำในขณะที่ (0); แฮ็คใน C ++?


233

เมื่อโฟลว์โค้ดเป็นดังนี้:

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

ฉันได้เห็นงานนี้ทั่วเพื่อหลีกเลี่ยงการไหลของรหัสยุ่งข้างต้น:

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

มีวิธีใดบ้างที่ดีกว่าที่จะหลีกเลี่ยงวิธีนี้เพื่อหลีกเลี่ยงการแฮ็กเพื่อให้เป็นโค้ดระดับสูงขึ้น (ระดับอุตสาหกรรม)

ข้อเสนอแนะใด ๆ ที่อยู่นอกกรอบยินดีต้อนรับ!


38
ไร่และโยนข้อยกเว้น
ta.speot.is

135
สำหรับฉันแล้วดูเหมือนจะเป็นเวลาที่เหมาะสมที่จะใช้goto- แต่ฉันแน่ใจว่ามีบางคนจะทำเครื่องหมายฉันไว้เพื่อแนะนำสิ่งนั้นดังนั้นฉันจึงไม่ได้เขียนคำตอบสำหรับผลกระทบนั้น การมี LONG นั้นdo ... while(0);ดูเหมือนว่าผิด
Mats Petersson

42
@dasblinkenlight: ใช่แน่นอน หากคุณกำลังจะใช้gotoจงซื่อสัตย์เกี่ยวกับมันและทำมันในที่โล่งอย่าซ่อนมันโดยใช้breakและdo ... while
Mats Petersson

44
@MatsPetersson: ให้คำตอบฉันจะให้ +1 สำหรับgotoความพยายามฟื้นฟูของคุณ :)
wilx

27
@ ta.speot.is: "RAII และโยนข้อยกเว้น" คุณจะจำลองการควบคุมการไหลด้วยข้อยกเว้น นั่นคือมันเหมือนกับการใช้ฮาร์ดแวร์ราคาแพงมีเลือดออกเหมือนค้อนหรือที่ทับกระดาษ คุณสามารถทำสิ่งนั้นได้ แต่นั่นดูเหมือนจะเป็นรสชาติที่แย่มากสำหรับฉัน
SigTerm

คำตอบ:


309

ถือว่าเป็นแนวปฏิบัติที่ยอมรับได้เพื่อแยกการตัดสินใจเหล่านี้ในฟังก์ชั่นและใช้returns แทนbreaks ในขณะที่การตรวจสอบทั้งหมดเหล่านี้สอดคล้องกับระดับนามธรรมของฟังก์ชั่นมันเป็นวิธีการที่ค่อนข้างตรรกะ

ตัวอย่างเช่น:

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

22
@MatsPetersson: "แยกในฟังก์ชั่น" หมายถึงการเปลี่ยนเป็นฟังก์ชันใหม่ซึ่งทำเฉพาะการทดสอบ
MSalters

35
+1 นี่เป็นคำตอบที่ดีเช่นกัน ใน C ++ 11 ฟังก์ชั่นที่แยกได้นั้นอาจเป็นแลมบ์ดาเนื่องจากมันยังสามารถจับตัวแปรท้องถิ่นได้ดังนั้นจึงทำให้ทุกอย่างง่ายขึ้น!
นาวาซ

4
@deworde: ขึ้นอยู่กับสถานการณ์ทางออกนี้อาจนานกว่าและอ่านน้อยกว่า goto เนื่องจาก C ++ โชคไม่ดีที่ไม่อนุญาตให้มีการ จำกัด ฟังก์ชั่นเฉพาะที่คุณจะต้องย้ายฟังก์ชั่นนั้น (อันที่คุณจะกลับมาจากที่อื่น) ซึ่งช่วยลดความสามารถในการอ่าน อาจจบลงด้วยพารามิเตอร์หลายสิบจากนั้นเนื่องจากการมีพารามิเตอร์หลายสิบเป็นสิ่งที่ไม่ดีคุณจะตัดสินใจที่จะรวมไว้ใน struct ซึ่งจะสร้างชนิดข้อมูลใหม่ พิมพ์มากเกินไปสำหรับสถานการณ์ง่ายๆ
SigTerm

24
@Damon หากมีสิ่งใดreturnสะอาดกว่าเพราะผู้อ่านทุกคนทราบทันทีว่าทำงานได้อย่างถูกต้องและทำงานอย่างไร ด้วยgotoคุณต้องมองไปรอบ ๆ เพื่อดูว่ามันคืออะไรและเพื่อให้แน่ใจว่าไม่มีข้อผิดพลาดเกิดขึ้น การปลอมตัวเป็นผลประโยชน์
R. Martinho Fernandes

11
@SigTerm: บางครั้งรหัสควรถูกย้ายไปยังฟังก์ชั่นที่แยกต่างหากเพียงเพื่อให้ขนาดของแต่ละฟังก์ชั่นลงไปขนาดที่ง่ายต่อการเหตุผล forceinlineหากคุณไม่ต้องการที่จะจ่ายสำหรับการโทรฟังก์ชั่นทำเครื่องหมาย
Ben Voigt

257

มีหลายครั้งที่การใช้gotoคำตอบที่ถูกต้องคืออย่างน้อยสำหรับคนที่ไม่เชื่อในศาสนาว่า " gotoไม่สามารถเป็นคำตอบได้ไม่ว่าคำถามจะเป็นอย่างไร" - และนี่เป็นหนึ่งในกรณีเหล่านั้น

รหัสนี้จะใช้สับของdo { ... } while(0);เพื่อวัตถุประสงค์ของการแต่งเนื้อแต่งตัวเป็นgoto breakหากคุณกำลังจะใช้gotoให้เปิดเกี่ยวกับมัน ไม่มีจุดในการทำให้รหัส HARDER อ่าน

สถานการณ์เฉพาะคือเมื่อคุณมีโค้ดจำนวนมากที่มีเงื่อนไขค่อนข้างซับซ้อน:

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

  }
  finish up. 
}

มันสามารถทำให้ชัดเจนว่ารหัสถูกต้องโดยไม่มีตรรกะที่ซับซ้อนเช่นนั้น

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

แก้ไข: เพื่อชี้แจงฉันไม่เคยเสนอการใช้gotoเป็นวิธีแก้ปัญหาทั่วไป แต่มีบางกรณีที่gotoเป็นทางออกที่ดีกว่าโซลูชันอื่น ๆ

ลองนึกภาพตัวอย่างว่าเรากำลังรวบรวมข้อมูลบางอย่างและเงื่อนไขที่แตกต่างกันที่กำลังทดสอบคือ "นี่คือจุดสิ้นสุดของข้อมูลที่รวบรวม" - ซึ่งขึ้นอยู่กับเครื่องหมาย "ต่อ / จบ" บางประเภทที่แตกต่างกันไปขึ้นอยู่กับตำแหน่ง คุณอยู่ในกระแสข้อมูล

ตอนนี้เมื่อเราทำเสร็จแล้วเราต้องบันทึกข้อมูลลงในไฟล์

และใช่มักจะมีโซลูชันอื่น ๆ ที่สามารถให้โซลูชันที่สมเหตุสมผล แต่ไม่เสมอไป


76
ไม่เห็นด้วย gotoอาจมีสถานที่ แต่goto cleanupไม่มี การล้างข้อมูลเสร็จสิ้นด้วย RAII
MSalters

14
@MSalters: นั่นถือว่าการล้างข้อมูลเกี่ยวข้องกับสิ่งที่สามารถแก้ไขได้ด้วย RAII บางทีฉันควรจะพูดว่า "ปัญหาข้อผิดพลาด" หรือบางอย่างแทน
Mats Petersson

25
ดังนั้นจงลงคะแนนในเรื่องนี้ต่อไปโดยสันนิษฐานว่ามาจากความเชื่อทางศาสนาที่ว่าgotoไม่ใช่คำตอบที่ถูกต้อง ฉันขอขอบคุณถ้ามีความคิดเห็น ...
เสื่อ Petersson

19
คนเกลียดgotoเพราะคุณต้องคิดที่จะใช้ / เพื่อทำความเข้าใจกับโปรแกรมที่ใช้มัน ... ไมโครโปรเซสเซอร์ในทางกลับกันถูกสร้างขึ้นjumpsและconditional jumps... ดังนั้นปัญหาก็คือกับบางคนไม่ใช่กับตรรกะหรืออย่างอื่น
woliveirajr

17
+1 gotoสำหรับการใช้งานที่เหมาะสมของ RAII ไม่ใช่วิธีแก้ไขที่ถูกต้องหากคาดว่าจะเกิดข้อผิดพลาด (ไม่ใช่ข้อยกเว้น) เนื่องจากเป็นการละเมิดข้อยกเว้น
โจ

82

คุณสามารถใช้รูปแบบการต่อเนื่องอย่างง่ายกับboolตัวแปร:

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

ห่วงโซ่ของการดำเนินการนี้จะหยุดทันทีที่ผลตอบแทนcheckN falseจะไม่มีการcheck...()โทรเพิ่มเติมเนื่องจากการลัดวงจรของ&&ผู้ควบคุมเครื่อง นอกจากนี้การเพิ่มประสิทธิภาพคอมไพเลอร์พอสมาร์ทที่จะยอมรับการตั้งค่าที่goOnจะfalseเป็นถนนทางเดียวและใส่ที่ขาดหายไปgoto endสำหรับคุณ ด้วยเหตุนี้ประสิทธิภาพของโค้ดข้างต้นจะเหมือนกับของ a do/ while(0)โดยไม่กระทบต่อการอ่านที่เจ็บปวด


30
การมอบหมายภายในifเงื่อนไขดูน่าสงสัยมาก
มิคาอิล

90
@ Mikail เฉพาะกับดวงตาที่ไม่ได้รับการฝึกฝน
dasblinkenlight

20
ฉันเคยใช้เทคนิคนี้มาก่อนแล้วมันก็บั๊กฉันเสมอฉันคิดว่าคอมไพเลอร์จะต้องสร้างรหัสเพื่อตรวจสอบแต่ละครั้งifไม่ว่าอะไรgoOnจะเกิดขึ้นแม้ว่าต้น ๆ จะล้มเหลวก็ตาม แต่ฉันเพิ่งทดสอบและอย่างน้อยก็ VS2012 ฉลาดพอที่จะลัดวงจรทุกอย่างหลังจากเท็จครั้งแรกอยู่ดี ฉันจะใช้สิ่งนี้บ่อยขึ้น หมายเหตุ:หากคุณใช้goOn &= checkN()แล้วcheckN()จะทำงานเสมอแม้ว่าจะgoOnเป็นfalseช่วงเริ่มต้นของif(เช่นอย่าทำอย่างนั้น)
ทำเครื่องหมาย

11
@Nawaz: ถ้าคุณมีความคิดที่ไม่ได้ฝึกฝนการเปลี่ยนแปลงโดยพลการทั่วฐานรหัสคุณมีปัญหาใหญ่กว่าการมอบหมายภายในifs
Idelic

6
@sisharp Elegance เป็นเช่นนั้นในสายตาของคนดู! ฉันล้มเหลวที่จะเข้าใจว่าในโลกนี้การใช้โครงสร้างวนรอบในทางที่ผิดอาจถูกมองว่าเป็น "สง่างาม" แต่บางทีนั่นอาจเป็นแค่ฉัน
dasblinkenlight

38
  1. ลองแยกรหัสออกเป็นฟังก์ชันแยกต่างหาก (หรืออาจมากกว่าหนึ่ง) จากนั้นส่งคืนจากฟังก์ชันหากการตรวจสอบล้มเหลว

  2. หากมันแน่นเกินไปกับรหัสรอบที่ทำเช่นนั้นและคุณไม่สามารถหาวิธีลดการคัปปลิ้งได้ให้ดูที่รหัสหลังจากบล็อกนี้ สันนิษฐานได้ว่ามันล้างทรัพยากรบางส่วนที่ใช้โดยฟังก์ชั่น พยายามที่จะจัดการทรัพยากรเหล่านี้โดยใช้วัตถุRAII ; จากนั้นแทนที่แต่ละ dodgy breakด้วยreturn(หรือthrowถ้าเหมาะสมกว่า) และให้ตัวทำลายวัตถุทำความสะอาดให้คุณ

  3. หากโฟลว์ของโปรแกรมเป็น (จำเป็น) อย่างที่คุณต้องการจริงๆgotoแล้วให้ใช้มันแทนการปลอมตัวแปลก ๆ

  4. หากคุณมีกฎการเข้ารหัสที่ห้ามมิให้สุ่มสี่สุ่มห้าgotoและคุณไม่สามารถทำให้การไหลเวียนของโปรแกรมง่ายขึ้นคุณอาจต้องปลอมแปลงมันด้วยdoแฮ็คของคุณ


4
ฉันส่งน้อมถ่อมตนว่า RAII ซึ่งมีประโยชน์ไม่ใช่กระสุนเงิน เมื่อคุณพบว่าตัวเองกำลังจะเขียนคลาส convert-goto-to-RAII ที่ไม่มีประโยชน์อะไรฉันคิดว่าคุณจะได้รับการบริการที่ดีขึ้นโดยใช้คำว่า "goto-end-of-the-world" คนที่กล่าวถึงแล้ว
idoby

1
@busy_wait: อันที่จริง RAII ไม่สามารถแก้ปัญหาได้ทุกอย่าง นั่นเป็นเหตุผลที่คำตอบของฉันไม่ได้หยุดอยู่แค่ในจุดที่สอง แต่ยังคงแนะนำgotoว่าเป็นตัวเลือกที่ดีกว่าจริง ๆ หรือไม่
Mike Seymour

3
ฉันเห็นด้วย แต่ฉันคิดว่ามันเป็นความคิดที่ดีที่จะเขียนคลาสแปลงของ goto-RAII และฉันคิดว่าควรระบุไว้อย่างชัดเจน
idoby

@idoby เกี่ยวกับการเขียนคลาสเทมเพลต RAII หนึ่งคลาสและสร้างอินสแตนซ์ด้วยการล้างข้อมูลแบบใช้ครั้งเดียวที่คุณต้องการในแลมบ์ดา
Caleth

@Caleth ฟังดูราวกับว่าคุณกำลังสร้าง ctors / dtors อยู่ใช่ไหม?
idoby

37

TLDR : RAII , รหัสการทำธุรกรรม (ตั้งค่าผลลัพธ์หรือส่งคืนเนื้อหาเมื่อคำนวณแล้ว) และข้อยกเว้น

คำตอบยาว:

ในซีแนวทางปฏิบัติที่ดีที่สุดสำหรับโค้ดประเภทนี้คือการเพิ่ม EXIT / CLEANUP / เลเบลอื่น ๆในโค้ดที่มีการล้างทรัพยากรท้องถิ่นเกิดขึ้นและส่งคืนรหัสข้อผิดพลาด (ถ้ามี) นี่คือแนวปฏิบัติที่ดีที่สุดเนื่องจากแยกรหัสตามธรรมชาติในการกำหนดค่าเริ่มต้นการคำนวณกระทำและส่งคืน:

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 ในโค้ดเบสส่วนใหญ่, if(error_ok != ...และgotoมักจะซ่อนอยู่ด้านหลังมาโครสะดวกซื้อบางตัว ( RET(computation_result),ENSURE_SUCCESS(computation_result, return_code)ฯลฯ )

C ++เสนอเครื่องมือพิเศษมากกว่า C :

  • ฟังก์ชันการล้างข้อมูลบล็อกสามารถใช้งานเป็น RAII ซึ่งหมายความว่าคุณไม่จำเป็นต้องใช้cleanupบล็อกทั้งหมดอีกต่อไปและเปิดใช้งานรหัสลูกค้าเพื่อเพิ่มข้อความสั่งคืนก่อนหน้า

  • คุณโยนเมื่อใดก็ตามที่คุณไม่สามารถดำเนินการต่อเปลี่ยนทั้งหมด if(error_ok != ...เป็นสายตรง

รหัส C ++ ที่เทียบเท่ากัน:

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

นี่เป็นแนวปฏิบัติที่ดีที่สุดเพราะ:

  • มันชัดเจน (นั่นคือในขณะที่การจัดการข้อผิดพลาดไม่ชัดเจนกระแสหลักของอัลกอริทึมคือ)

  • มันง่ายที่จะเขียนรหัสลูกค้า

  • มันน้อยที่สุด

  • มันง่าย

  • มันไม่มีการสร้างรหัสซ้ำ

  • มันไม่ใช้มาโคร

  • มันไม่ได้ใช้do { ... } while(0)โครงสร้างที่แปลก

  • มันสามารถนำกลับมาใช้ใหม่ได้โดยใช้ความพยายามน้อยที่สุด (นั่นคือถ้าฉันต้องการคัดลอกการเรียกไปcomputation2();ยังฟังก์ชันอื่นฉันไม่ต้องตรวจสอบให้แน่ใจว่าฉันเพิ่มdo { ... } while(0)รหัสใหม่ในโค้ดหรือ#defineแมโค wrapper และป้ายล้างข้อมูลหรือ สิ่งอื่นใด)


+1 นี่คือสิ่งที่ฉันพยายามใช้โดยใช้ RAII หรืออะไรบางอย่าง shared_ptrด้วย deleter ที่กำหนดเองสามารถทำสิ่งต่าง ๆ มากมาย ง่ายยิ่งขึ้นกับ lambdas ใน C ++ 11
Macke

จริง แต่ถ้าคุณใช้ shared_ptr สำหรับสิ่งที่ไม่ใช่พอยน์เตอร์ให้ลองพิมพ์อย่างน้อย: namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };ในกรณีนี้ (ด้วยmake_handleการตั้งค่าประเภทเดลเตอร์ที่ถูกต้องในการก่อสร้าง) ชื่อประเภทไม่แนะนำอีกต่อไปมันเป็นตัวชี้ .
utnapistim

21

ฉันกำลังเพิ่มคำตอบเพื่อความสมบูรณ์ คำตอบอื่น ๆ จำนวนมากชี้ให้เห็นว่าบล็อกเงื่อนไขขนาดใหญ่สามารถแบ่งออกเป็นฟังก์ชันแยกต่างหาก แต่ตามที่ได้กล่าวไว้หลายครั้งก็คือวิธีการนี้จะแยกรหัสที่มีเงื่อนไขออกจากบริบทดั้งเดิม นี่คือเหตุผลหนึ่งที่เพิ่ม lambdas ให้กับภาษาใน C ++ 11 คนอื่นแนะนำให้ใช้ lambdas แต่ไม่มีการแจกตัวอย่างที่ชัดเจน ฉันได้ใส่ไว้ในคำตอบนี้ สิ่งที่ทำให้ฉันประทับใจคือมันให้ความรู้สึกคล้ายกับdo { } while(0)วิธีการหลายวิธี - และนั่นอาจหมายถึงว่ามันยังgotoแฝงตัวอยู่ ...

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

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

7
สำหรับฉันแฮ็คนี้ดูแย่กว่าสิ่งที่ต้องทำ ... ในขณะที่แฮ็ค
Michael

18

ไม่แน่นอนคำตอบ แต่คำตอบ (เพื่อประโยชน์ของความสมบูรณ์)

แทน :

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

คุณสามารถเขียน:

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

นี่ยังคงเป็นgoto ที่ปลอมตัว แต่อย่างน้อยก็ไม่ได้วนซ้ำอีกต่อไป ซึ่งหมายความว่าคุณจะไม่ต้องตรวจสอบอย่างรอบคอบว่าไม่มีใครดำเนินการต่อซ่อนอยู่ที่ไหนสักแห่งในบล็อก

การสร้างนั้นง่ายพอที่คุณสามารถหวังว่าคอมไพเลอร์จะปรับมันให้เหมาะสม

ตามที่แนะนำโดย @jamesdlin คุณสามารถซ่อนไว้ด้านหลังแมโครอย่างเช่น

#define BLOC switch(0) case 0:

และใช้งานได้เหมือนกัน

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

สิ่งนี้เป็นไปได้เนื่องจากไวยากรณ์ภาษา C คาดว่าจะมีคำสั่งหลังสวิตช์ไม่ใช่บล็อกที่อยู่ในวงเล็บและคุณสามารถใส่ป้ายชื่อเคสไว้ข้างหน้าคำสั่งนั้นได้ จนถึงตอนนี้ฉันไม่เห็นจุดอนุญาตให้ทำเช่นนั้น แต่ในกรณีนี้มันมีประโยชน์ที่จะซ่อนสวิตช์ไว้ด้านหลังมาโครที่ดี


2
โอวฉลาด คุณสามารถซ่อนมันไว้หลังมาโครเช่นนั้นdefine BLOCK switch (0) case 0:และใช้มันBLOCK { ... break; }ได้
jamesdlin

@jamesdlin: มันไม่เคยเกิดขึ้นกับฉันมาก่อนว่ามันจะเป็นประโยชน์ tu ใส่เคสไว้ก่อนวงเล็บเปิดของสวิตช์ แต่มันได้รับอนุญาตจาก C และในกรณีนี้มันสะดวกในการเขียนมาโครที่ดี
kriss

15

gotoฉันจะแนะนำวิธีการคล้ายกับเสื่อคำตอบลบที่ไม่จำเป็น ใส่ตรรกะตามเงื่อนไขในฟังก์ชันเท่านั้น รหัสใด ๆ ที่ทำงานอยู่เสมอควรไปก่อนหรือหลังฟังก์ชันถูกเรียกใช้ในผู้เรียก:

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
หากคุณต้องได้รับทรัพยากรอื่นในช่วงกลางของfuncคุณจะต้องแยกออกจากฟังก์ชั่นอื่น (ต่อรูปแบบของคุณ) หากฟังก์ชันที่แยกได้เหล่านี้ต้องการข้อมูลเดียวกันคุณเพียงแค่คัดลอกอาร์กิวเมนต์สแต็กเดียวกันซ้ำแล้วซ้ำอีกหรือตัดสินใจที่จะจัดสรรอาร์กิวเมนต์ของคุณในฮีปและส่งตัวชี้ไปรอบ ๆ ไม่ใช่ใช้ประโยชน์จากคุณลักษณะพื้นฐานของภาษามากที่สุด อาร์กิวเมนต์ของฟังก์ชัน) กล่าวโดยย่อคือฉันไม่เชื่อว่าโซลูชันนี้จะปรับขนาดสำหรับกรณีที่เลวร้ายที่สุดซึ่งมีการตรวจสอบทุกเงื่อนไขหลังจากได้รับทรัพยากรใหม่ที่ต้องทำความสะอาด หมายเหตุ Nawaz แสดงความคิดเห็นเกี่ยวกับ lambdas อย่างไรก็ตาม
TNE

2
ฉันจำ OP ไม่ได้เลยว่าพูดอะไรเกี่ยวกับการรับทรัพยากรตรงกลางโค้ดบล็อกของเขา แต่ฉันยอมรับว่ามันเป็นข้อกำหนดที่เป็นไปได้ ในกรณีเช่นนี้จะเกิดอะไรขึ้นกับการประกาศทรัพยากรบนสแต็คที่ใดก็ได้ในfunc()และอนุญาตให้ destructor จัดการทรัพยากรที่ว่าง? หากมีสิ่งใดนอกเหนือจากfunc()ความต้องการเข้าถึงทรัพยากรเดียวกันควรมีการประกาศบนฮีปก่อนที่จะเรียกfunc()โดยผู้จัดการทรัพยากรที่เหมาะสม
Dan Bechard

12

การไหลของรหัสตัวเองเป็นกลิ่นรหัสที่เกิดขึ้นมากในฟังก์ชั่น หากไม่มีวิธีการแก้ปัญหาโดยตรง (ฟังก์ชั่นนี้เป็นฟังก์ชั่นตรวจสอบทั่วไป) จากนั้นใช้RAIIเพื่อให้คุณสามารถกลับมาแทนที่จะกระโดดไปที่ส่วนท้ายของฟังก์ชันได้ดีกว่า


11

หากคุณไม่จำเป็นต้องแนะนำตัวแปรท้องถิ่นในระหว่างการดำเนินการจากนั้นคุณสามารถทำให้แบนนี้ได้บ่อย:

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

// edit 
doThingsAtEndAndReportErrorStatus()

2
แต่เงื่อนไขแต่ละข้อจะต้องรวมเงื่อนไขก่อนหน้าซึ่งไม่เพียง แต่น่าเกลียด แต่อาจส่งผลเสียต่อประสิทธิภาพการทำงาน ดีกว่าที่จะข้ามไปตรวจสอบและล้างข้อมูลทันทีที่เรารู้ว่าเรา "ไม่ตกลง"
TNE

นั่นเป็นเรื่องจริงอย่างไรก็ตามวิธีการนี้มีข้อดีหากมีสิ่งที่ต้องทำในตอนท้ายดังนั้นคุณไม่ต้องการกลับมาเร็ว (ดูการแก้ไข) หากคุณรวมกับแนวทางของเดนิสในการใช้บูลในเงื่อนไขการเข้าชมการทำงานจะไม่ได้รับการยกเว้นเว้นแต่จะอยู่ในวงที่แน่นมาก
the_mandrill

10

คล้ายกับคำตอบของ dasblinkenlight แต่หลีกเลี่ยงการมอบหมายภายในifที่อาจได้รับ "แก้ไข" โดยผู้ตรวจสอบรหัส:

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

...

ฉันใช้รูปแบบนี้เมื่อต้องตรวจสอบผลลัพธ์ของขั้นตอนก่อนขั้นตอนถัดไปซึ่งแตกต่างจากสถานการณ์ที่การตรวจสอบทั้งหมดสามารถทำได้ล่วงหน้าด้วยif( check1() && check2()...รูปแบบขนาดใหญ่


โปรดทราบว่า check1 () อาจเป็น PerformStep1 () ซึ่งส่งคืนรหัสผลลัพธ์ของขั้นตอน สิ่งนี้จะช่วยลดความซับซ้อนของฟังก์ชันโฟลว์กระบวนการของคุณ
เดนิส Skidmore

10

ใช้ข้อยกเว้น รหัสของคุณจะดูสะอาดกว่า (และมีการสร้างข้อยกเว้นสำหรับการจัดการข้อผิดพลาดในโฟลว์การเรียกใช้งานของโปรแกรม) สำหรับการล้างทรัพยากร (ตัวอธิบายไฟล์การเชื่อมต่อฐานข้อมูล ฯลฯ ) ให้อ่านบทความเหตุใด C ++ จึงไม่สร้างโครงสร้าง "ในที่สุด" .

#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
จริงๆ? ก่อนอื่นฉันสุจริตไม่เห็นการปรับปรุงการอ่านใด ๆ ที่นั่น ห้ามใช้ข้อยกเว้นเพื่อควบคุมการไหลของโปรแกรม นั่นไม่ใช่จุดประสงค์ที่เหมาะสมของพวกเขา นอกจากนี้ข้อยกเว้นยังนำมาซึ่งการลงโทษที่มีนัยสำคัญ กรณีการใช้งานที่ถูกต้องสำหรับข้อยกเว้นคือเมื่อมีเงื่อนไขบางอย่างที่คุณไม่สามารถทำสิ่งใด ๆ ได้เช่นพยายามเปิดไฟล์ที่ถูกล็อกโดยกระบวนการอื่นโดยเฉพาะหรือการเชื่อมต่อเครือข่ายล้มเหลวหรือการโทรไปยังฐานข้อมูลล้มเหลว หรือผู้เรียกส่งพารามิเตอร์ที่ไม่ถูกต้องไปยังกระบวนการ เรื่องแบบนั้น แต่อย่าใช้ข้อยกเว้นเพื่อควบคุมการไหลของโปรแกรม
Craig

3
ฉันหมายถึงไม่ต้องขี้เกียจเกี่ยวกับเรื่องนี้ แต่ไปที่บทความ Stroustrup ที่คุณอ้างอิงและค้นหา "ฉันควรใช้ข้อยกเว้นเพื่ออะไร" มาตรา. เหนือสิ่งอื่นใดเขาพูดว่า: "โดยเฉพาะการโยนไม่ใช่แค่ทางเลือกในการคืนค่าจากฟังก์ชั่น (คล้ายกับการกลับมา) การทำเช่นนี้จะช้าและจะทำให้โปรแกรมเมอร์ C ++ ส่วนใหญ่ใช้เพื่อยกเว้นข้อผิดพลาดเท่านั้น การจัดการในทำนองเดียวกันการโยนไม่ใช่วิธีที่ดีในการออกจากวง "
Craig

3
@Craig ทุกอย่างที่คุณได้ชี้ไปนั้นถูกต้อง แต่คุณกำลังสมมติว่าโปรแกรมตัวอย่างสามารถดำเนินการต่อหลังจากcheck()เงื่อนไขล้มเหลว สมมติว่าโปรแกรมไม่สามารถดำเนินการต่อได้การใช้ข้อยกเว้นเป็นวิธีที่จะดำเนิน
Cartucho

2
จริงอยู่ถ้านั่นเป็นบริบทจริงๆ แต่เรื่องตลกเกี่ยวกับสมมติฐาน ... ;-)
Craig

10

สำหรับฉันdo{...}while(0)เป็นเรื่องปกติ หากคุณไม่ต้องการที่จะเห็นdo{...}while(0)คุณสามารถกำหนดคำหลักทางเลือกสำหรับพวกเขา

ตัวอย่าง:

//--------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

ฉันคิดว่าคอมไพเลอร์จะลบwhile(0)เงื่อนไขที่ไม่จำเป็นในdo{...}while(0)ในรุ่นไบนารีและแปลงตัวแบ่งเป็นการกระโดดแบบไม่มีเงื่อนไข คุณสามารถตรวจสอบเวอร์ชั่นภาษาแอสเซมบลีเพื่อให้แน่ใจ

การใช้gotoยังสร้างโค้ดที่สะอาดขึ้นและมันตรงไปตรงมากับลอจิกจากนั้นกระโดด คุณสามารถทำสิ่งต่อไปนี้:

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

หมายเหตุ: }ฉลากถูกวางไว้หลังจากปิด นี่คือการหลีกเลี่ยงปัญหาที่เป็นไปได้หนึ่งข้อในgotoการวางรหัสโดยบังเอิญเพราะคุณไม่เห็นป้ายกำกับ ตอนนี้มันเป็นเหมือนdo{...}while(0)ไม่มีรหัสเงื่อนไข

ในการทำให้โค้ดนี้สะอาดและเข้าใจง่ายขึ้นคุณสามารถทำได้:

//--------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)

ด้วยสิ่งนี้คุณสามารถทำบล็อกซ้อนกันและระบุตำแหน่งที่คุณต้องการออก / กระโดดออก

//--------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)

รหัสสปาเก็ตตี้ไม่ใช่ความผิดของgotoมันเป็นความผิดของโปรแกรมเมอร์ คุณยังสามารถสร้างรหัสสปาเก็ตตี้ได้โดยไม่ต้องใช้gotoคุณยังคงสามารถผลิตรหัสปาเก็ตตี้โดยไม่ต้องใช้


10
ฉันเลือกที่จะgotoขยายไวยากรณ์ภาษาโดยใช้ตัวประมวลผลล่วงหน้ากว่าล้านครั้ง
คริสเตียน

2
"ในการทำให้โค้ดนี้สะอาดและเข้าใจง่ายขึ้นคุณสามารถ [ใช้ LOADS_OF_WEIRD_MACROS]" : ไม่คำนวณ
underscore_d

8

นี่เป็นปัญหาที่รู้จักกันดีและได้รับการแก้ไขเป็นอย่างดีจากมุมมองการเขียนโปรแกรมใช้งานซึ่งอาจเป็น monad

เพื่อตอบสนองต่อความคิดเห็นที่ฉันได้รับด้านล่างฉันได้แก้ไขคำแนะนำของฉันที่นี่: คุณสามารถดูรายละเอียดทั้งหมดเกี่ยวกับการนำC ++ monads ไปใช้ในสถานที่ต่าง ๆซึ่งจะช่วยให้คุณบรรลุสิ่งที่ Rotsor แนะนำ ต้องใช้เวลาสักครู่ในการคลาน monads ดังนั้นแทนที่จะฉันจะแนะนำที่นี่กลไก monad เหมือน "คนจน" อย่างรวดเร็วซึ่งคุณจำเป็นต้องรู้เกี่ยวกับอะไรมากกว่าการเพิ่ม :: ตัวเลือก

ตั้งค่าขั้นตอนการคำนวณของคุณดังนี้:

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

เห็นได้ชัดว่าแต่ละขั้นตอนการคำนวณสามารถทำอะไรบางอย่างเช่นผลตอบแทนboost::noneถ้ามันเป็นตัวเลือกที่ได้รับจะว่างเปล่า ตัวอย่างเช่น:

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

จากนั้นเชื่อมโยงพวกเขาเข้าด้วยกัน:

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
}

สิ่งที่ดีเกี่ยวกับเรื่องนี้คือคุณสามารถเขียนการทดสอบหน่วยที่กำหนดไว้อย่างชัดเจนสำหรับแต่ละขั้นตอนการคำนวณ นอกจากนี้การเรียกใช้จะอ่านเช่นภาษาอังกฤษธรรมดา (ตามปกติในกรณีที่มีสไตล์การทำงาน)

หากคุณไม่สนใจเกี่ยวกับความไม่สามารถเปลี่ยนแปลงได้และจะสะดวกกว่าที่จะคืนค่าวัตถุเดียวกันทุกครั้งที่คุณพบกับรูปแบบที่เปลี่ยนแปลงโดยใช้ shared_ptr หรือสิ่งที่คล้ายกัน


3
รหัสนี้มีคุณสมบัติที่ไม่พึงประสงค์ของการบังคับให้แต่ละฟังก์ชั่นของแต่ละบุคคลที่จะจัดการกับความล้มเหลวของฟังก์ชั่นก่อนหน้านี้จึงไม่เหมาะสมใช้ Monad สำนวน (ที่ผลกระทบ monadic ในกรณีนี้ความล้มเหลว ในการทำเช่นนั้นคุณต้องมีoptional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);แทนและใช้การดำเนินการเรียงองค์ประกอบ monadic ('ผูก') แทนการใช้ฟังก์ชั่น
Rotsor

ขอบคุณ คุณถูกต้อง - นั่นคือวิธีที่จะทำอย่างถูกต้อง ฉันไม่ต้องการที่จะเขียนมากเกินไปในคำตอบของฉันที่จะอธิบายว่า (ดังนั้นคำว่า "ชายผู้น่าสงสาร" ซึ่งตั้งใจจะแนะนำว่าฉันจะไม่ไปที่นี่ทั้งหมู)
เบเนดิกต์

7

วิธีการเกี่ยวกับการย้ายคำสั่ง if เป็นฟังก์ชั่นพิเศษที่ให้ผลลัพธ์เป็นตัวเลขหรือ enum

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

สิ่งนี้ดีเมื่อเป็นไปได้ แต่โดยทั่วไปน้อยกว่าคำถามที่ถามที่นี่ ทุกเงื่อนไขอาจขึ้นอยู่กับรหัสที่ดำเนินการหลังจากการทดสอบการแยกส่วนครั้งล่าสุด
kriss

ปัญหาที่นี่คือคุณแยกสาเหตุและผลที่ตามมา คือคุณแยกรหัสที่อ้างถึงหมายเลขเงื่อนไขเดียวกันและนี่อาจเป็นแหล่งที่มาของข้อบกพร่องเพิ่มเติม
ริกา

@kriss: ก็สามารถปรับฟังก์ชั่น ConditionCode () เพื่อดูแลสิ่งนี้ได้ จุดสำคัญของฟังก์ชั่นนั้นคือคุณสามารถใช้ return <result> เพื่อการออกแบบ clean ทันทีที่คำนวณเงื่อนไขสุดท้าย และนั่นคือสิ่งที่ให้ความชัดเจนของโครงสร้างที่นี่
karx11erx

@Riga: Imo เหล่านี้เป็นข้อคัดค้านทางวิชาการ ฉันพบว่า C ++ มีความซับซ้อนมากขึ้นเป็นความลับและอ่านไม่ได้ในทุกเวอร์ชันใหม่ ฉันไม่เห็นปัญหากับฟังก์ชั่นตัวช่วยขนาดเล็กประเมินสภาพที่ซับซ้อนด้วยวิธีการที่มีโครงสร้างเพื่อให้ฟังก์ชั่นเป้าหมายด้านล่างอ่านง่ายขึ้น
karx11erx

1
@ karx11erx การคัดค้านของฉันมีประโยชน์และขึ้นอยู่กับประสบการณ์ของฉัน รูปแบบนี้ไม่ดีอย่างไม่เกี่ยวข้องกับ C ++ 11 หรือภาษาใดก็ตาม หากคุณมีปัญหากับการสร้างภาษาที่ช่วยให้คุณสามารถเขียนสถาปัตยกรรมที่ดีที่ไม่ใช่ปัญหาทางภาษา
ริกา

5

บางอย่างเช่นนี้อาจจะ

#define EVER ;;

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

หรือใช้ข้อยกเว้น

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

การใช้ข้อยกเว้นคุณสามารถส่งผ่านข้อมูลได้


10
โปรดอย่าทำสิ่งที่ฉลาดเช่นคำจำกัดความของคุณพวกเขามักจะทำให้อ่านรหัสยากขึ้นสำหรับนักพัฒนาเพื่อน ฉันเคยเห็นคนกำหนด Case เป็นตัวแบ่ง case ในไฟล์ส่วนหัวและใช้ใน switch ในไฟล์ cpp ทำให้คนอื่นสงสัยหลายชั่วโมงว่าทำไม switch ถึงแตกระหว่างคำสั่ง Case Grrr ...
Michael

5
และเมื่อคุณตั้งชื่อแมโครคุณควรทำให้มันดูเหมือนมาโคร (เช่นในตัวพิมพ์ใหญ่ทั้งหมด) มิฉะนั้นคนที่เกิดขึ้นกับชื่อตัวแปร / ฟังก์ชั่น / ประเภท / ฯลฯ ชื่อeverจะมีความสุขมาก ...
jamesdlin

5

ฉันไม่ได้ใช้วิธีการเฉพาะbreakหรือreturnในกรณีเช่นนี้โดยเฉพาะ ระบุว่าโดยปกติเมื่อเราเผชิญสถานการณ์ดังกล่าวมักเป็นวิธีการที่ค่อนข้างยาว

หากเรามีจุดออกหลายจุดอาจทำให้เกิดปัญหาเมื่อเราต้องการรู้ว่าอะไรจะทำให้ตรรกะบางอย่างถูกดำเนินการ: โดยปกติแล้วเราจะขึ้นไปบนบล็อคที่ล้อมรอบส่วนของตรรกะนั้นและเกณฑ์ของบล็อกล้อมรอบนั้นบอกเราว่า สถานการณ์:

ตัวอย่างเช่น,

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

โดยการดูบล็อกที่ล้อมรอบมันเป็นเรื่องง่ายที่จะค้นหาว่าmyLogic()จะเกิดขึ้นเฉพาะเมื่อconditionA and conditionB and conditionCเป็นจริงเท่านั้น

มันจะมองเห็นได้น้อยลงเมื่อมีการคืนต้น:

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

เราไม่สามารถนำทางจากอีกต่อไป myLogic()โดยดูที่การปิดล้อมบล็อกเพื่อกำหนดเงื่อนไข

มีวิธีแก้ไขปัญหาต่าง ๆ ที่ฉันใช้อยู่ นี่คือหนึ่งในนั้น:

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

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

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

(แน่นอนว่ามันยินดีที่จะใช้ตัวแปรเดียวกันเพื่อแทนที่ทั้งหมด isA isB isC )

วิธีการดังกล่าวอย่างน้อยจะให้ผู้อ่านของรหัสที่จะดำเนินการเมื่อmyLogic() isB && conditionCผู้อ่านได้รับคำใบ้ว่าเขาต้องค้นหาเพิ่มเติมว่าอะไรจะทำให้ isB เป็นจริง


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

แล้วเรื่องนั้นล่ะ


ถ้าล้าสมัยเพียงแค่return condition;อย่างอื่นฉันคิดว่านี่คือการบำรุงรักษาที่ดี
SpaceTrucker

2

รูปแบบอื่นที่มีประโยชน์หากคุณต้องการขั้นตอนการล้างข้อมูลที่แตกต่างกันขึ้นอยู่กับความล้มเหลว:

    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

ฉันไม่ใช่โปรแกรมเมอร์C ++ดังนั้นฉันจะไม่เขียนโค้ดใด ๆ ที่นี่ แต่จนถึงขณะนี้ยังไม่มีใครได้พูดถึงวิธีแก้ปัญหาเชิงวัตถุ ดังนั้นที่นี่ฉันเดาว่า

มีอินเตอร์เฟสทั่วไปที่จัดเตรียมวิธีการประเมินเงื่อนไขเดียว ตอนนี้คุณสามารถใช้รายการการใช้งานของเงื่อนไขเหล่านั้นในวัตถุของคุณที่มีวิธีการที่เป็นปัญหา คุณวนซ้ำในรายการและประเมินแต่ละเงื่อนไขซึ่งอาจแตกเร็วขึ้นหากมีข้อผิดพลาด

สิ่งที่ดีคือการออกแบบดังกล่าวยึดติดกับหลักการเปิด / ปิดได้ดีเพราะคุณสามารถเพิ่มเงื่อนไขใหม่ในระหว่างการเริ่มต้นของวัตถุที่มีวิธีการที่เป็นปัญหา คุณยังสามารถเพิ่มวิธีที่สองในอินเทอร์เฟซด้วยวิธีการประเมินสภาพที่ส่งคืนคำอธิบายของเงื่อนไข สามารถใช้สำหรับระบบการจัดทำเอกสารด้วยตนเอง

อย่างไรก็ตามข้อเสียคือมีค่าใช้จ่ายเพิ่มขึ้นเล็กน้อยเนื่องจากการใช้วัตถุมากขึ้นและการวนซ้ำในรายการ


คุณสามารถเพิ่มตัวอย่างเป็นภาษาอื่นได้ไหม ฉันคิดว่าคำถามนี้ใช้กับหลายภาษาแม้ว่าจะถูกถามเฉพาะเกี่ยวกับ C ++
Denise Skidmore

1

นี่คือวิธีที่ฉันทำ

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

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

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

1

ขั้นแรกเป็นตัวอย่างสั้น ๆ เพื่อแสดงว่าทำไมจึงgotoไม่ใช่วิธีแก้ปัญหาที่ดีสำหรับ C ++:

struct Bar {
    Bar();
};

extern bool check();

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

    Bar x;

    out:
}

ลองรวบรวมสิ่งนี้เป็นไฟล์วัตถุและดูว่าเกิดอะไรขึ้น จากนั้นลองใช้doเครื่องหมายbreak+ +while(0) +

นั่นคือการกัน ประเด็นหลักดังต่อไปนี้

โค้ดขนาดเล็กเหล่านั้นมักต้องการการล้างข้อมูลบางอย่างหากฟังก์ชันทั้งหมดล้มเหลว การสะสางเหล่านั้นมักจะเกิดขึ้นในลำดับที่ตรงกันข้ามจากกลุ่มก้อนเองในขณะที่คุณ "คลาย" การคำนวณที่เสร็จสิ้นแล้วบางส่วน

ทางเลือกหนึ่งในการรับความหมายเหล่านี้คือRAII ; ดูคำตอบของ @ utnapistim C ++ รับประกันว่า destructors อัตโนมัติจะทำงานในลำดับตรงกันข้ามกับ constructors ซึ่งโดยธรรมชาติจะเป็น "คลี่คลาย"

แต่นั่นต้องใช้คลาส RAII มากมาย บางครั้งตัวเลือกที่ง่ายกว่าก็คือใช้สแต็ก:

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

... และต่อไป ง่ายต่อการตรวจสอบเนื่องจากจะใส่รหัส "เลิกทำ" ถัดจากรหัส "ทำ" การตรวจสอบง่ายเป็นสิ่งที่ดี นอกจากนี้ยังทำให้การควบคุมการไหลชัดเจนมาก มันเป็นรูปแบบที่มีประโยชน์สำหรับ C เช่นกัน

มันอาจต้องการcalcฟังก์ชั่นในการรับอาร์กิวเมนต์มากมาย แต่โดยปกติจะไม่เป็นปัญหาหากคลาส / โครงสร้างของคุณมีการเชื่อมโยงที่ดี (นั่นคือสิ่งที่อยู่ร่วมกันอาศัยอยู่ในวัตถุเดียวดังนั้นฟังก์ชั่นเหล่านี้สามารถใช้เป็นตัวชี้หรือการอ้างอิงถึงวัตถุจำนวนน้อยและยังทำงานที่มีประโยชน์มากมาย)


ง่ายมากในการตรวจสอบเส้นทางการล้างข้อมูล แต่อาจไม่ง่ายนักในการติดตามเส้นทางทองคำ แต่โดยรวมแล้วฉันคิดว่าสิ่งนี้จะส่งเสริมรูปแบบการล้างข้อมูลที่สอดคล้องกัน
เดนิส Skidmore

0

หากคุณมีรหัสบล็อกยาวของ if..else if..else งบคุณอาจลองและเขียนบล็อกทั้งหมดด้วยความช่วยเหลือของหรือFunctors function pointersมันอาจไม่ใช่ทางออกที่ถูกต้องเสมอไป แต่บ่อยครั้งก็คือ

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


ในหลักการนี้เป็นไปได้ (และไม่เลว) แต่ด้วยฟังก์ชั่นวัตถุที่ชัดเจนหรือตัวชี้มันเพียงรบกวนการไหลของรหัสไกลเกินไป OTOH the เทียบเท่ากับการใช้ lambdas หรือฟังก์ชั่นที่มีชื่อสามัญคือแนวปฏิบัติที่ดีมีประสิทธิภาพและอ่านได้ดี
leftaroundabout

0

ฉันประหลาดใจกับจำนวนคำตอบที่ต่างกันที่นำเสนอที่นี่ แต่ในที่สุดในรหัสที่ฉันต้องเปลี่ยน (เช่นลบdo-while(0)แฮ็คนี้หรืออะไรก็ได้) ฉันทำสิ่งที่แตกต่างจากคำตอบที่กล่าวถึงที่นี่และฉันสับสนว่าทำไมไม่มีใครคิดเรื่องนี้ นี่คือสิ่งที่ฉันทำ:

รหัสเริ่มต้น:

do {

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

finishingUpStuff.

ขณะนี้:

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

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

ดังนั้นสิ่งที่ทำที่นี่คือการตกแต่งสิ่งที่แยกได้ในฟังก์ชั่นและสิ่งที่กลายเป็นเรียบง่ายและสะอาด!

ฉันคิดว่าวิธีนี้คุ้มค่าที่จะพูดถึงดังนั้นให้มาที่นี่


0

รวมเข้าไว้ในifคำสั่งเดียว:

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

นี่เป็นรูปแบบที่ใช้ในภาษาต่างๆเช่น Java ซึ่งมีการลบคำหลัก goto


2
ใช้งานได้เฉพาะในกรณีที่คุณไม่ต้องทำอะไรระหว่างการทดสอบเงื่อนไข (ดีฉันคิดว่าคุณสามารถซ่อนสิ่งที่ทำในการเรียกใช้ฟังก์ชั่นบางอย่างที่ทำโดยการทดสอบเงื่อนไข แต่อาจได้รับความสับสนเล็กน้อยถ้าคุณทำมันมากเกินไป)
Jeremy Friesner

@JeremyFriesner trueที่จริงแล้วคุณจริงสามารถทำในระหว่างสิ่งที่เป็นฟังก์ชั่นบูลที่แยกต่างหากที่มักจะประเมินเป็น การประเมินผลการลัดวงจรจะรับประกันได้ว่าคุณไม่เคยทำงานในระหว่างที่การทดสอบเบื้องต้นไม่ได้ผ่าน
AJMansfield

@JMansfield ใช่นั่นคือสิ่งที่ฉันอ้างถึงในประโยคที่สองของฉัน ... แต่ฉันไม่แน่ใจว่าการทำเช่นนั้นจะเป็นการปรับปรุงคุณภาพรหัส
Jeremy Friesner

@JeremyFriesner ไม่มีอะไรทำให้คุณไม่สามารถเขียนเงื่อนไขได้เพราะ(/*do other stuff*/, /*next condition*/)คุณสามารถจัดรูปแบบได้อย่างสวยงาม อย่าคาดหวังว่าคนจะชอบมัน แต่อย่างสุจริตนี้เพียงไปเพื่อแสดงให้เห็นว่ามันเป็นความผิดพลาดสำหรับ Java เฟสให้เห็นว่าgotoคำสั่ง ...
cmaster - คืนสิทธิ์ monica

@JeremyFriesner ฉันคิดว่านี่เป็นบูลีน หากฟังก์ชั่นถูกเรียกใช้ภายในเงื่อนไขแต่ละข้อมีวิธีที่ดีกว่าในการจัดการมัน
Tyzoid

0

หากใช้ตัวจัดการข้อผิดพลาดเดียวกันสำหรับข้อผิดพลาดทั้งหมดและแต่ละขั้นตอนจะส่งกลับค่าบูลที่ระบุว่าสำเร็จ:

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

(คล้ายกับคำตอบของ tyzoid แต่เงื่อนไขเป็นการกระทำและ && ป้องกันการกระทำเพิ่มเติมที่เกิดขึ้นหลังจากความล้มเหลวครั้งแรก)


0

ทำไมไม่มีการตั้งค่าสถานะวิธีตอบถูกใช้มาตั้งแต่สมัยโบราณ

//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
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.