มีเหตุผลจริงหรือไม่ที่ทำให้เกิดการ && และ || มากเกินไป อย่าลัดวงจร?


137

พฤติกรรมการลัดวงจรของผู้ปฏิบัติงาน&&และ||เป็นเครื่องมือที่ยอดเยี่ยมสำหรับโปรแกรมเมอร์

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


1
@PiotrS คำถามนั้นน่าจะเป็นคำตอบ ฉันเดาว่ามาตรฐานสามารถกำหนดไวยากรณ์ใหม่เพื่อจุดประสงค์นี้เท่านั้น อาจเป็น operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht

1
@PiotrS .: พิจารณาตรรกะ {true, false, nil}Tri-State: เนื่องจากnil&& x == nilอาจทำให้เกิดไฟฟ้าลัดวงจร
MSalters

1
@MSalters: ลองคิดดูสิstd::valarray<bool> a, b, c;ว่าคุณa || b || cจะลัดวงจรได้อย่างไร?
Piotr Skotnicki

4
@PiotrS: ฉันกำลังเถียงว่ามีอยู่อย่างน้อยหนึ่งประเภทที่ไม่ใช่บูลสำหรับการลัดวงจรที่ทำให้เกิดการลัดวงจร ฉันไม่ได้โต้แย้งว่าการทำให้สั้นลงนั้นเหมาะสมสำหรับทุกประเภทที่ไม่ใช่บูล
MSalters

3
ยังไม่มีใครพูดถึงเรื่องนี้ แต่ก็ยังมีปัญหาเรื่องความเข้ากันได้แบบย้อนหลัง เว้นแต่จะได้รับการดูแลเป็นพิเศษเพื่อ จำกัด สถานการณ์ที่การลัดวงจรนี้จะนำไปใช้การลัดวงจรเช่นนี้อาจทำลายรหัสที่มีอยู่ซึ่งเกินพิกัดoperator&&หรือoperator||ขึ้นอยู่กับตัวถูกดำเนินการทั้งสองที่ถูกประเมิน การรักษาความเข้ากันได้ย้อนหลังเป็นสิ่งสำคัญ (หรือควร) เมื่อเพิ่มคุณสมบัติให้กับภาษาที่มีอยู่
David Hammen

คำตอบ:


151

กระบวนการออกแบบทั้งหมดส่งผลให้เกิดการประนีประนอมระหว่างเป้าหมายที่เข้ากันไม่ได้ น่าเสียดายที่กระบวนการออกแบบสำหรับ&&ผู้ดำเนินการที่มีการโอเวอร์โหลดใน C ++ นั้นสร้างผลลัพธ์ที่น่าสับสน: ซึ่งจะไม่มีการแสดงคุณลักษณะที่คุณต้องการจาก&&พฤติกรรมการลัดวงจรที่สั้นมาก

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

หนึ่งในคำตอบอื่น ๆ ชี้ให้เห็น "แลมบ์ดายก" นั่นคือ:

A && B

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

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

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

นี่ไม่ใช่สิ่งที่ทีมออกแบบ C # ทำ (นอกเหนือจาก: แม้ว่าการยกแลมบ์ดาเป็นสิ่งที่ฉันทำเมื่อถึงเวลาที่ต้องทำนิพจน์การเป็นตัวแทนของ??โอเปอเรเตอร์ซึ่งต้องการการดำเนินการแปลงบางอย่างที่จะดำเนินการอย่างเกียจคร้านอธิบายว่าในรายละเอียด ใช้งานได้ แต่มีน้ำหนักมากพอที่เราต้องการหลีกเลี่ยง)

แต่วิธีการแก้ปัญหา C # แบ่งปัญหาออกเป็นสองปัญหาที่แยกจากกัน:

  • เราควรประเมินตัวถูกดำเนินการทางขวาหรือไม่?
  • ถ้าคำตอบข้างต้นคือ "ใช่" แล้วเราจะรวมตัวถูกดำเนินการทั้งสองได้อย่างไร?

ดังนั้นจึงแก้ปัญหาได้โดยทำให้ผิดกฎหมายเกินพิกัด&&โดยตรง แต่ใน C # คุณต้องโอเวอร์โหลดโอเปอเรเตอร์สองตัวซึ่งแต่ละอันตอบคำถามหนึ่งในสองข้อนั้น

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

(นอกเหนือจาก: จริง ๆ สามข้อ C # ต้องการว่าหากfalseมีการให้โอเปอเรเตอร์แล้วtrueต้องให้โอเปอเรเตอร์ซึ่งตอบคำถาม: สิ่งนี้คือ "true-ish?" โดยทั่วไปแล้วไม่มีเหตุผลที่จะให้โอเปอเรเตอร์เดียว ต้องใช้ทั้งคู่)

พิจารณาข้อความของแบบฟอร์ม:

C cresult = cleft && cright;

คอมไพเลอร์สร้างรหัสสำหรับสิ่งนี้ตามที่คิดว่าคุณเขียน pseudo-C # นี้:

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

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

ตัว||ดำเนินการถูกกำหนดในลักษณะที่คล้ายคลึงกันเป็นการเรียกใช้ตัวดำเนินการจริงและตัว|ดำเนินการกระตือรือร้น:

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

โดยการกำหนดทั้งสี่ผู้ประกอบการ - true, false, &และ|- C # ช่วยให้คุณไม่เพียง แต่พูดcleft && crightแต่ยังไม่ใช่ลัดวงจรcleft & crightและif (cleft) if (cright) ...และc ? consequence : alternativeและwhile(c)และอื่น ๆ

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

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

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


1
@Dupuplicator: คุณอาจสนใจอ่านคำถามและคำตอบนี้: stackoverflow.com/questions/5965968/…
Eric Lippert

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

1
@EricLippert ฉันเชื่อว่า Envision ระบุว่าเขาเห็นโพสต์นี้และคิดว่าเป็นคุณ ... จากนั้นก็เห็นว่าเขาพูดถูก เขาไม่ได้พูดว่าyour postไม่เกี่ยวข้อง His noticing your distinct writing styleไม่เกี่ยวข้อง
WernerCD

5
ทีม Microsoft ไม่ได้รับเครดิตเพียงพอสำหรับ (1) พยายามอย่างดีในการทำสิ่งที่ถูกต้องใน C # และ (2) ทำให้ถูกต้องมากกว่าเวลา
codenheim

2
@Voo: หากคุณเลือกที่จะใช้การแปลงโดยปริยายเพื่อboolคุณสามารถใช้&&และ||ไม่ใช้operator true/falseหรือoperator &/|ไม่มีปัญหาใน C # ปัญหาเกิดขึ้นอย่างแม่นยำในสถานการณ์ที่ไม่มีการแปลงboolเป็นไปได้หรือในกรณีที่ไม่ต้องการ
Eric Lippert

43

ประเด็นคือ (ภายในขอบเขตของ C ++ 98) ตัวถูกดำเนินการทางขวาจะถูกส่งผ่านไปยังฟังก์ชันตัวดำเนินการที่โอเวอร์โหลดเป็นอาร์กิวเมนต์ ในการทำเช่นนั้นมันจะถูกประเมินแล้ว ไม่มีอะไรที่เป็นoperator||()หรือoperator&&()รหัสสามารถหรือไม่สามารถทำอะไรที่จะหลีกเลี่ยงปัญหานี้

ผู้ประกอบการดั้งเดิมนั้นแตกต่างกันเพราะไม่ใช่ฟังก์ชั่น แต่มีการใช้งานในระดับที่ต่ำกว่าของภาษา

ฟีเจอร์ภาษาเพิ่มเติมอาจทำให้การประเมินตัวถูกดำเนินการทางด้านขวานั้นไม่สามารถทำได้ อย่างไรก็ตามพวกเขาไม่ได้กังวลเพราะมีเพียงไม่กี่กรณีเท่านั้นที่จะมีประโยชน์ทางความหมาย (เช่นเดียวกับ? :ที่ไม่สามารถโหลดได้มากเกินไป

(ใช้เวลา 16 ปีในการนำลูกแกะเข้าสู่มาตรฐาน ... )

สำหรับการใช้ความหมายพิจารณา:

objectA && objectB

สิ่งนี้ทำให้เดือดลงไปที่:

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

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

และถ้าคุณกำลังเรียกร้องการแปลงบูลดี ...

objectA && obectB

ทำสิ่งเดียวกันตอนนี้หรือไม่ ดังนั้นทำไมเกินในครั้งแรก?


7
ข้อผิดพลาดทางตรรกะของคุณคือเหตุผลภายในภาษาที่กำหนดในปัจจุบันเกี่ยวกับผลกระทบของภาษาที่กำหนดแตกต่างกัน ในสมัยก่อนมีมือใหม่จำนวนมากที่ทำสิ่งนั้น "ตัวสร้างเสมือน" ต้องใช้คำอธิบายจำนวนมากเกินกว่าที่จะนำพวกเขาออกจากการคิดแบบกล่อง อย่างไรก็ตามด้วยการลัดวงจรของตัวดำเนินการในตัวมีการรับประกันเกี่ยวกับการโต้แย้งที่ไม่ใช่การประเมินผล การรับประกันดังกล่าวจะอยู่ที่นั่นสำหรับการโอเวอร์โหลดที่ผู้ใช้กำหนดหากการลัดวงจรถูกกำหนดไว้สำหรับพวกเขา
ไชโยและ hth - Alf

1
@iFreilicht: โดยพื้นฐานแล้วฉันพูดแบบเดียวกับ Deduplicator หรือ Piotr ด้วยคำพูดที่ต่างกัน ฉันอธิบายรายละเอียดเล็กน้อยในประเด็นในคำตอบที่แก้ไขแล้ว วิธีนี้สะดวกกว่ามากไม่มีการขยายภาษาที่จำเป็น (เช่น lambdas) จนกระทั่งเมื่อไม่นานมานี้และผลประโยชน์จะได้รับเพียงเล็กน้อย ไม่กี่ครั้งที่คนที่รับผิดชอบจะต้อง "ชอบ" สิ่งที่ผู้สร้างคอมไพเลอร์ไม่ได้ทำไปแล้วในปี 1998 มันกลับมาแล้ว (ดูexport)
DevSolar

9
@iFreilicht: boolโอเปอเรเตอร์การแปลงสำหรับคลาสใดก็ได้สามารถเข้าถึงตัวแปรสมาชิกทั้งหมดได้เช่นกันและทำงานได้ดีกับโอเปอเรเตอร์ builtin สิ่งอื่นนอกจากการแปลงเป็นบูลนั้นไม่สมเหตุสมผลสำหรับการประเมินการลัดวงจร! ลองวิธีนี้จากมุมมองความหมายไม่ได้เป็นประโยคที่หนึ่ง: สิ่งที่คุณจะได้รับการพยายามที่จะบรรลุไม่ว่าคุณจะไปเกี่ยวกับเรื่องนี้
DevSolar

1
ฉันต้องยอมรับว่าฉันไม่สามารถคิดได้ เหตุผลเดียวที่ทำให้เกิดการลัดวงจรเกิดขึ้นเพราะช่วยประหยัดเวลาในการดำเนินการกับ Booleans และคุณสามารถทราบผลการแสดงออกก่อนที่จะมีการโต้แย้งทั้งหมดที่กำลังถูกประเมิน ด้วยการดำเนินการอื่น ๆ และนั่นไม่ใช่กรณีและนั่นคือสาเหตุ&และ&&ไม่ใช่ตัวดำเนินการเดียวกัน ขอบคุณที่ช่วยให้ฉันตระหนักว่า
iFreilicht

8
@iFreilicht: แต่วัตถุประสงค์ของการไฟฟ้าลัดวงจรเป็นเพราะการคำนวณทางด้านซ้ายมือสามารถสร้างความจริงของเงื่อนไขของทางด้านขวามือที่ if (x != NULL && x->foo)ต้องใช้การลัดวงจรไม่ใช้ความเร็ว แต่เพื่อความปลอดภัย
Eric Lippert

26

คุณสมบัติจะต้องคำนึงถึงการออกแบบการใช้งานเอกสารและการจัดส่ง

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


ในทางทฤษฎีผู้ปฏิบัติงานทุกคนสามารถอนุญาตให้มีพฤติกรรมการลัดวงจรเพียงภาษาเดียวที่มี " ภาษารอง" เพิ่มเติมในภาษา C ++ 11 (เมื่อมีการแนะนำ lambdas, 32 ปีหลังจาก "C กับคลาส" เริ่มในปี 2522 หลังจาก c ++ 98):

C ++ จะต้องมีวิธีในการใส่คำอธิบายข้อโต้แย้งที่ขี้เกียจ - ประเมิน - แลมบ์ดา - เพื่อหลีกเลี่ยงการประเมินผลจนจำเป็นและได้รับอนุญาต (พบเงื่อนไขก่อน)


คุณลักษณะทางทฤษฎีจะมีลักษณะอย่างไร (โปรดจำไว้ว่าคุณลักษณะใหม่ใด ๆ ควรใช้งานได้อย่างกว้างขวาง)

คำอธิบายประกอบlazyซึ่งใช้กับฟังก์ชั่นอาร์กิวเมนต์ทำให้ฟังก์ชั่นแม่แบบที่คาดหวังว่านักแสดงและทำให้คอมไพเลอร์แพ็คการแสดงออกใน functor:

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

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

มันจะมีหน้าตาเหมือน:

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

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


นอกเหนือจากความซับซ้อนของการติดตั้งใช้งานและความซับซ้อนทางแนวคิด (ทุก ๆ คุณสมบัติเพิ่มขึ้นทั้งสองอย่างยกเว้นว่ามันจะช่วยลดความซับซ้อนเหล่านั้นให้เพียงพอสำหรับคุณสมบัติอื่น ๆ ) ให้ดูที่การพิจารณาที่สำคัญอื่น ๆ : ความเข้ากันได้ย้อนหลัง

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

BTW: คุณลักษณะนี้ในขณะที่ใช้งานง่ายกว่านั้นจะแข็งแกร่งกว่าโซลูชัน C # ในการแยก&&และแบ่งออก||เป็นสองฟังก์ชั่นสำหรับการแยกคำจำกัดความ


6
@iFreilicht: คำถามใด ๆ ของแบบฟอร์ม "ทำไมฟีเจอร์ X ถึงไม่มีอยู่จริง?" มีคำตอบที่เหมือนกัน: การมีอยู่ของคุณสมบัติจะต้องได้รับการพิจารณาให้ถือว่าเป็นความคิดที่ดีออกแบบระบุดำเนินการทดสอบจัดทำเอกสารและจัดส่งไปยังผู้ใช้ปลายทาง หากสิ่งใดสิ่งหนึ่งเหล่านี้ไม่ได้เกิดขึ้นไม่มีคุณสมบัติ หนึ่งในสิ่งเหล่านั้นไม่ได้เกิดขึ้นกับคุณสมบัติที่คุณเสนอ การค้นหาว่าอันไหนเป็นปัญหาการวิจัยเชิงประวัติศาสตร์ เริ่มพูดคุยกับผู้คนในคณะกรรมการออกแบบหากคุณสนใจสิ่งใดสิ่งหนึ่งที่ไม่เคยทำ
Eric Lippert

1
@EricLippert: และทำซ้ำจนกว่าจะมีการใช้เหตุผล: บางทีมันอาจจะคิดว่าซับซ้อนเกินไปและไม่มีใครคิดว่าจะทำการประเมินอีกครั้ง หรือการประเมินใหม่สิ้นสุดลงด้วยเหตุผลที่แตกต่างกันที่จะปฏิเสธมากกว่าที่ถือไว้ก่อนหน้า (btw: เพิ่มส่วนสำคัญของความคิดเห็นของคุณ)
Deduplicator

@Dupuplicator ด้วยเทมเพลตนิพจน์ไม่จำเป็นต้องใช้คีย์เวิร์ด lazy หรือ lambdas
Sumant

โปรดทราบว่าภาษาอัลกอล 68 ดั้งเดิมมีการบังคับ "ขั้นตอน" (เช่นเดียวกับการลดทอนซึ่งหมายถึงการเรียกใช้ฟังก์ชันแบบไม่มีพารามิเตอร์โดยปริยายเมื่อบริบทต้องการประเภทผลลัพธ์มากกว่าประเภทของฟังก์ชัน) ซึ่งหมายความว่าการแสดงออกของประเภท T ในตำแหน่งที่ต้องใช้ค่าประเภท "ฟังก์ชั่น parameterless กลับ T" (สะกด " proc T" ใน Algol 68) จะถูกเปลี่ยนเป็นร่างกายโดยปริยายกลับหน้าที่แสดงออกโดยปริยายแลมบ์ดา คุณลักษณะดังกล่าวถูกลบออก (ไม่เหมือนกับ deproceduring) ในการแก้ไขภาษาในปี 1973
Marc van Leeuwen

... สำหรับ C ++ แนวทางที่คล้ายกันอาจเป็นการประกาศโอเปอเรเตอร์เช่นเดียว&&กับการใช้อาร์กิวเมนต์หนึ่งตัวในประเภท "ตัวชี้ไปยังฟังก์ชันที่ส่งคืน T" และกฎการแปลงเพิ่มเติมที่อนุญาตให้มีการแสดงออกของอาร์กิวเมนต์ประเภท T โปรดทราบว่านี่ไม่ใช่การแปลงปกติเนื่องจากจะต้องทำที่ระดับ syntactic: การเปลี่ยนค่า runtime ประเภท T เป็นฟังก์ชันจะไม่มีประโยชน์เนื่องจากการประเมินผลจะเสร็จสิ้นแล้ว
Marc van Leeuwen

13

ด้วยการหาเหตุผลเข้าข้างตนเองย้อนหลังส่วนใหญ่เป็นเพราะ

  • เพื่อรับประกันการลัดวงจร (โดยไม่แนะนำไวยากรณ์ใหม่) ผู้ประกอบการจะต้องถูก จำกัด ผลอาร์กิวเมนต์แรกที่สามารถแปลงเป็นboolจริงและ

  • การลัดวงจรสามารถแสดงได้อย่างง่ายดายในวิธีอื่นเมื่อจำเป็น


ตัวอย่างเช่นถ้าชั้นTมีความเกี่ยวข้อง&&และ||ผู้ประกอบการแล้วการแสดงออก

auto x = a && b || c;

ที่a, bและcมีการแสดงออกของชนิดTสามารถนำมาแสดงมีการลัดวงจรได้

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

หรืออาจชัดเจนยิ่งขึ้นว่า

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

ความซ้ำซ้อนที่เห็นได้ชัดช่วยรักษาผลข้างเคียงใด ๆ จากการเรียกใช้ตัวดำเนินการ


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

ฉันไม่แน่ใจทั้งหมดของการปฏิบัติตามมาตรฐานของทั้งหมดต่อไปนี้ (ยังคงเป็นบิตของ influensa) แต่มันคอมไพล์อย่างสะอาดด้วย Visual C ++ 12.0 (2013) และ MinGW g ++ 4.8.2:

#include <iostream>
using namespace std;

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

struct S
{
    using Op_result = S;

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

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

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

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

};

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

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

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

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

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

เอาท์พุท:

000 -> !! !! || เท็จ
001 -> !! !! || จริง
010 -> !! !! || เท็จ
011 -> !! !! || จริง
100 -> !! && !! || เท็จ
101 -> !! && !! || จริง
110 -> !! && !! จริง
111 -> !! && !! จริง

ที่นี่!!บางปังแสดงให้เห็นถึงการแปลงboolคือการตรวจสอบค่าอาร์กิวเมนต์

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


ฉันชอบการลัดวงจรของคุณโดยเฉพาะอย่างยิ่งผู้ประกอบการซึ่งใกล้เคียงที่สุดเท่าที่คุณจะได้รับ
iFreilicht

คุณขาดการลัดวงจรของ&&- จะต้องมีบรรทัดเพิ่มเติมเช่นif (!a) { return some_false_ish_T(); }- และกระสุนแรกของคุณ: การลัดวงจรเป็นเรื่องเกี่ยวกับพารามิเตอร์ที่สามารถแปลงเป็นบูลไม่ใช่ผลลัพธ์
Arne Mertz

@ArneMertz: ความคิดเห็นของคุณเกี่ยวกับ "ไม่มี" เห็นได้ชัดว่าไม่มีความหมาย ความคิดเห็นเกี่ยวกับสิ่งที่เกี่ยวกับใช่ฉันรู้ว่า การแปลงboolเป็นสิ่งที่จำเป็นในการทำการลัดวงจร
ไชโยและ hth - Alf

@ Cheersandhth. -Alf ความคิดเห็นเกี่ยวกับการหายสำหรับการแก้ไขครั้งแรกของคำตอบของคุณที่คุณทำลัดวงจรที่แต่ไม่|| &&ความคิดเห็นอื่น ๆ มีวัตถุประสงค์ที่ "จะต้องถูก จำกัด ให้ผลลัพธ์ที่แปลงเป็นบูล" ในสัญลักษณ์แสดงหัวข้อย่อยแรกของคุณ - คุณควรอ่าน "จำกัดพารามิเตอร์ที่เปลี่ยนแปลงได้เพื่อบูล" imo
Arne Mertz

@ArneMertz: ตกลงเวอร์ชันใหม่ขอโทษฉันแก้ไขช้า ไม่ จำกัด ไม่ว่าจะเป็นผลลัพธ์ของตัวดำเนินการที่ต้องถูก จำกัด เนื่องจากจะต้องถูกแปลงเป็นboolเพื่อตรวจสอบการลัดวงจรของผู้ดำเนินการเพิ่มเติมในนิพจน์ เช่นเดียวกับผลของการa && bจะต้องมีการแปลงboolเพื่อตรวจสอบสั้น circuting a && b || cของตรรกะหรือ
ไชโยและ hth - Alf

5

tl; dr : มันไม่คุ้มค่ากับความพยายามเนื่องจากมีความต้องการต่ำมาก (ใครจะใช้คุณสมบัตินี้) เปรียบเทียบกับค่าใช้จ่ายที่ค่อนข้างสูง (จำเป็นต้องใช้ไวยากรณ์พิเศษ)

สิ่งแรกที่ควรคำนึงถึงคือการใช้งานตัวดำเนินการมากเกินไปเป็นวิธีแฟนซีในการเขียนฟังก์ชั่นในขณะที่เวอร์ชันบูลีนของโอเปอเรเตอร์||และ&&เป็นของ buitlin นั่นหมายความว่าคอมไพเลอร์มีอิสระในการลัดวงจรพวกเขาในขณะที่การแสดงออกx = y && zกับ nonboolean yและมีจะนำไปสู่การเรียกร้องให้ฟังก์ชั่นเช่นz X operator&& (Y, Z)นี่หมายถึงว่าy && zเป็นวิธีแฟนซีในการเขียนoperator&&(y,z)ซึ่งเป็นเพียงการเรียกใช้ฟังก์ชันที่มีชื่อแปลก ๆ ซึ่งพารามิเตอร์ทั้งสองจะต้องถูกประเมินก่อนเรียกฟังก์ชัน (รวมถึงสิ่งใดก็ตามที่จะถือว่าเป็นการลัดวงจรที่เหมาะสม)

แต่หนึ่งได้ยืนยันว่ามันควรจะเป็นไปได้ที่จะทำให้การแปลของ&&ผู้ประกอบการที่ค่อนข้างมีความซับซ้อนมากขึ้นเช่นมันเป็นสำหรับnewผู้ประกอบการที่จะแปลเป็นฟังก์ชั่นการโทรoperator newตามด้วยโทรคอนสตรัค

เทคนิคนี้จะไม่มีปัญหาใครจะต้องกำหนดไวยากรณ์ภาษาเฉพาะสำหรับเงื่อนไขที่เปิดใช้งานการลัดวงจร อย่างไรก็ตามการใช้ไฟฟ้าลัดวงจรจะถูก จำกัด ในกรณีที่Yไม่สามารถใช้Xงานได้มิฉะนั้นจะต้องมีข้อมูลเพิ่มเติมเกี่ยวกับการลัดวงจรจริง (เช่นคำนวณผลลัพธ์จากพารามิเตอร์แรกเท่านั้น) ผลลัพธ์จะต้องมีลักษณะดังนี้:

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

หนึ่งไม่ค่อยต้องการเกินพิกัดoperator||และoperator&&เนื่องจากมีไม่ค่อยมีกรณีที่การเขียนa && bจริงเป็นธรรมชาติในบริบท nonboolean ข้อยกเว้นเดียวที่ฉันรู้คือเทมเพลตนิพจน์เช่นสำหรับ DSL แบบฝัง และมีเพียงไม่กี่รายเท่านั้นที่จะได้รับประโยชน์จากการประเมินการลัดวงจร โดยปกติแล้วเทมเพลตนิพจน์จะไม่ทำเช่นนั้นเนื่องจากจะใช้สร้างฟอร์มนิพจน์ที่ประเมินในภายหลังดังนั้นคุณจึงจำเป็นต้องใช้ทั้งสองด้านของนิพจน์เสมอ

ในระยะสั้น: ค่าคอมไพเลอร์นักเขียนมิได้มาตรฐานผู้เขียนรู้สึกว่าจำเป็นต้องกระโดดผ่านห่วงและกำหนดและใช้ไวยากรณ์ที่ยุ่งยากเพิ่มเติมเพียงเพราะหนึ่งในล้านอาจได้รับความคิดที่ว่ามันจะดีที่จะมีการลัดวงจรในที่ผู้ใช้กำหนดoperator&&และoperator||- เพียงแค่ เพื่อให้ได้ข้อสรุปว่าการพยายามเขียนลอจิกต่อมือไม่น้อยกว่านั้น


ค่าใช้จ่ายนั้นสูงจริงๆเหรอ? ภาษาการเขียนโปรแกรม D อนุญาตให้ประกาศพารามิเตอร์lazyซึ่งเปลี่ยนนิพจน์ที่กำหนดเป็นอาร์กิวเมนต์โดยปริยายในฟังก์ชันที่ไม่ระบุชื่อ สิ่งนี้จะให้ทางเลือกที่เรียกใช้ฟังก์ชันเรียกอาร์กิวเมนต์นั้นหรือไม่ ดังนั้นหากภาษามีแลมบ์อยู่แล้วไวยากรณ์ที่จำเป็นเพิ่มเติมนั้นเล็กมาก ” Pseudocode”: X และ (A, ขี้เกียจ B) {ถ้า (cond (a)) {กลับสั้น (a); } อื่น {จริง (a, b ()); }}
BlackJack

@BlackJack ว่าพารามิเตอร์ lazy สามารถนำไปใช้ได้โดยการยอมรับ a std::function<B()>ซึ่งจะทำให้เกิดค่าใช้จ่ายที่แน่นอน หรือถ้าคุณยินดีที่จะ inline template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}มันทำให้มัน และอาจโอเวอร์โหลดด้วยBพารามิเตอร์"ปกติ" ดังนั้นผู้โทรสามารถตัดสินใจว่าจะเลือกเวอร์ชันใด lazyไวยากรณ์อาจจะสะดวกมากขึ้น แต่มีประสิทธิภาพการถ่วงดุลอำนาจบางอย่าง
Arne Mertz

1
หนึ่งในปัญหาที่เกิดขึ้นกับstd::functionเมื่อเทียบกับlazyคือครั้งแรกที่สามารถประเมินได้หลายครั้ง พารามิเตอร์ lazy fooซึ่งใช้ตามที่foo+fooยังถูกประเมินเพียงครั้งเดียว
MSalters

"การใช้งานวงจรไฟฟ้าลัดวงจรจะถูก จำกัด เฉพาะกรณีที่ Y ไม่สามารถเทียบกับ X ได้" ... ไม่มันถูก จำกัด เฉพาะกรณีที่Xสามารถคำนวณได้Yตามลำพัง แตกต่างกันมาก std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. นอกจากว่าคุณกำลังใช้ "Conversion" อย่างไม่เป็นทางการ
Mooing Duck

1
@Sumant พวกเขาสามารถ แต่คุณสามารถเขียนตรรกะของการลัดวงจรที่กำหนดเองoperator&&ด้วยมือ คำถามไม่ได้ถ้าเป็นไปได้ แต่ทำไมไม่มีวิธีที่สะดวกสั้น ๆ
Arne Mertz

5

แลมบ์ดาไม่ใช่วิธีเดียวที่จะแนะนำความขี้เกียจ การประเมิน Lazy ค่อนข้างตรงไปตรงมาโดยใช้เทมเพลตนิพจน์ใน C ++ ไม่จำเป็นต้องใช้คำหลักlazyและสามารถนำไปใช้ใน C ++ 98 ต้นไม้แสดงอารมณ์ได้กล่าวถึงข้างต้นแล้ว เทมเพลตนิพจน์นั้นเป็นต้นไม้แสดงอารมณ์ของมนุษย์ที่น่าสงสาร (แต่ฉลาด) เคล็ดลับคือการแปลงนิพจน์เป็นแผนผังของอินสแตนซ์ที่ซ้อนกันซ้ำ ๆ ของExprแม่แบบ ต้นไม้จะถูกประเมินแยกต่างหากหลังจากการก่อสร้าง

ดำเนินต่อไปรหัสลัดวงจร&&และ||ผู้ประกอบการสำหรับการเรียนSตราบใดที่มันยังมีบริการlogical_andและฟังก์ชั่นฟรีและมันเป็นแปลงสภาพให้แก่logical_or boolรหัสอยู่ใน C ++ 14 แต่แนวคิดนี้ใช้ใน C ++ 98 ได้เช่นกัน ดูตัวอย่างจริง

#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

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

จริงๆแล้วมีเหตุผลทำไมโอเวอร์โหลด&&และ||ไม่เกิดไฟฟ้าลัดวงจร?

ตัวดำเนินการโลจิคัลที่โอเวอร์โหลดแบบกำหนดเองไม่จำเป็นต้องทำตามตรรกะของตารางความจริงเหล่านี้

แต่ทำไมพวกเขาถึงสูญเสียพฤติกรรมนี้เมื่อทำงานหนักเกินไป?

ดังนั้นฟังก์ชั่นทั้งหมดจะต้องมีการประเมินตามปกติ คอมไพเลอร์จะต้องถือว่ามันเป็นตัวดำเนินการโอเวอร์โหลดแบบปกติ

ผู้คนโอเวอร์โหลดตัวดำเนินการเชิงตรรกะด้วยเหตุผลหลายประการ ตัวอย่างเช่น; พวกเขาอาจมีความหมายเฉพาะในโดเมนเฉพาะที่ไม่ใช่คนธรรมดา "ตรรกะ" คนคุ้นเคย


4

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


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

แน่นอนว่ามันจะเป็นคุณสมบัติที่ซับซ้อนเนื่องจากเราต้องคาดเดาคำจำกัดความของผู้ใช้มัน!
nj-ath

สิ่งที่เกี่ยวกับ: (<condition>)หลังจากการประกาศประกอบการเพื่อระบุเงื่อนไขที่อาร์กิวเมนต์ที่สองไม่ได้รับการประเมิน?
iFreilicht

@iFreilicht: คุณยังคงต้องการฟังก์ชั่นตัวเลือก unary
MSalters

3

แต่ตัวดำเนินการสำหรับบูลมีลักษณะการทำงานนี้ทำไมจึงควร จำกัด ประเภทเดียวนี้

ฉันแค่อยากตอบส่วนนี้ เหตุผลคือบิวด์อิน&&และ||นิพจน์ไม่ถูกนำไปใช้กับฟังก์ชันเนื่องจากตัวดำเนินการโอเวอร์โหลด

การมีตรรกะการลัดวงจรในตัวเพื่อความเข้าใจของคอมไพเลอร์เกี่ยวกับนิพจน์เฉพาะนั้นเป็นเรื่องง่าย มันเหมือนกับโฟลว์การควบคุมในตัวอื่น ๆ

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


1
ฉันสงสัยว่าการพิจารณาใด ๆ ให้กับคำถามที่ว่าทับถมของ&&, ||และ,ควรได้รับอนุญาต? ความจริงที่ว่า C ++ ไม่มีกลไกที่อนุญาตให้โอเวอร์โหลดทำงานเหมือนอย่างอื่นนอกจากการเรียกใช้ฟังก์ชันอธิบายว่าทำไมโอเวอร์โหลดของฟังก์ชั่นเหล่านั้นไม่สามารถทำสิ่งอื่นได้ แต่มันไม่ได้อธิบายว่าทำไมตัวดำเนินการเหล่านั้น ฉันสงสัยว่าเหตุผลที่แท้จริงคือพวกเขาถูกโยนในรายชื่อผู้ประกอบการโดยไม่ต้องคิดมาก
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.