วิธีที่สวยงามที่สุดในการเขียน 'if' แบบครั้งเดียว


138

เนื่องจาก C ++ 17 เราสามารถเขียนifบล็อกที่จะดำเนินการได้ทันทีดังนี้:

#include <iostream>
int main() {
    for (unsigned i = 0; i < 10; ++i) {

        if (static bool do_once = true; do_once) { // Enter only once
            std::cout << "hello one-shot" << std::endl;
            // Possibly much more code
            do_once = false;
        }

    }
}

ฉันรู้ว่าฉันอาจคิดมากเกินไปและมีวิธีอื่นในการแก้ปัญหานี้ แต่ก็ยัง - เป็นไปได้ไหมที่จะเขียนแบบนี้ดังนั้นจึงไม่จำเป็นต้องมีdo_once = falseในตอนท้าย?

if (DO_ONCE) {
    // Do stuff
}

ฉันคิดว่าเป็นฟังก์ชันตัวช่วยdo_once()ที่มีstatic bool do_onceแต่ถ้าฉันต้องการใช้ฟังก์ชันเดียวกันนี้ในที่ต่างๆล่ะ นี่อาจเป็นเวลาและสถานที่สำหรับ#define? ฉันหวังว่าจะไม่


53
ทำไมไม่เพียงif (i == 0)? มันชัดเจนเพียงพอ
SilvanoCerza

26
@SilvanoCerza เพราะนั่นไม่ใช่ประเด็น if-block นี้อาจอยู่ที่ไหนสักแห่งในฟังก์ชันบางอย่างที่ดำเนินการหลายครั้งไม่ใช่ในลูปปกติ
nada

8
อาจstd::call_onceเป็นตัวเลือก (ใช้สำหรับเธรด แต่ก็ยังใช้งานได้)
fdan

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

15
มันไม่ได้เกิดขึ้นกับฉันที่ตัวแปรเริ่มต้นในifเงื่อนไขอาจเป็นstaticได้ ที่ฉลาด
HolyBlackCat

คำตอบ:


145

ใช้std::exchange:

if (static bool do_once = true; std::exchange(do_once, false))

คุณสามารถทำให้การย้อนกลับค่าความจริงสั้นลง:

if (static bool do_once; !std::exchange(do_once, true))

แต่ถ้าคุณใช้สิ่งนี้มากอย่ามัว แต่คิดและสร้างกระดาษห่อหุ้มแทน:

struct Once {
    bool b = true;
    explicit operator bool() { return std::exchange(b, false); }
};

และใช้มันเช่น:

if (static Once once; once)

ตัวแปรไม่ควรอ้างอิงนอกเงื่อนไขดังนั้นชื่อจึงไม่ซื้อเรามากนัก โดยได้รับแรงบันดาลใจจากภาษาอื่น ๆ เช่นPythonซึ่งให้ความหมายพิเศษกับ_ตัวระบุเราอาจเขียนว่า:

if (static Once _; _)

การปรับปรุงเพิ่มเติม: ใช้ประโยชน์จากส่วน BSS (@Deduplicator) หลีกเลี่ยงการเขียนหน่วยความจำเมื่อเราเรียกใช้ (@ShadowRanger) ไปแล้วและให้คำแนะนำการทำนายสาขาหากคุณจะทดสอบหลาย ๆ ครั้ง (เช่นในคำถาม):

// GCC, Clang, icc only; use [[likely]] in C++20 instead
#define likely(x) __builtin_expect(!!(x), 1)

struct Once {
    bool b = false;
    explicit operator bool()
    {
        if (likely(b))
            return false;

        b = true;
        return true;
    }
};

33
ฉันรู้ว่ามาโครได้รับความเกลียดชังเป็นอย่างมากใน C ++ แต่มันก็ดูสะอาดมาก: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))เพื่อใช้เป็น:ONLY_ONCE { foo(); }
Fibbles

5
ฉันหมายถึงถ้าคุณเขียน "ครั้งเดียว" สามครั้งให้ใช้มากกว่าสามครั้งหากข้อความนั้นคุ้มค่า imo
อลัน

13
ชื่อ_นี้ใช้ในซอฟต์แวร์จำนวนมากเพื่อทำเครื่องหมายสตริงที่แปลได้ คาดหวังสิ่งที่น่าสนใจที่จะเกิดขึ้น
Simon Richter

1
หากคุณมีทางเลือกให้กำหนดค่าเริ่มต้นของสถานะคงที่เป็นศูนย์ทั้งหมด รูปแบบที่ปฏิบัติการได้ส่วนใหญ่มีความยาวของพื้นที่ทั้งหมดเป็นศูนย์
Deduplicator

7
การใช้_ตัวแปรจะมี un-Pythonic คุณไม่ได้ใช้_สำหรับตัวแปรที่จะอ้างอิงในภายหลังเพียงเพื่อเก็บค่าที่คุณต้องระบุตัวแปร แต่คุณไม่ต้องการค่า โดยทั่วไปจะใช้สำหรับการเปิดกล่องเมื่อคุณต้องการเพียงค่าบางค่าเท่านั้น (มีกรณีการใช้งานอื่น ๆ แต่ค่อนข้างแตกต่างจากกรณีมูลค่าการทิ้ง)
jpmc26

91

อาจจะไม่ได้เป็นทางออกที่สวยงามมากที่สุดและคุณไม่เห็นจริง ๆifแต่ห้องสมุดมาตรฐานจริงครอบคลุมกรณีนี้ :, std::call_onceดู

#include <mutex>

std::once_flag flag;

for (int i = 0; i < 10; ++i)
    std::call_once(flag, [](){ std::puts("once\n"); });

ข้อดีคือนี่คือเธรดที่ปลอดภัย


3
ฉันไม่ทราบ std :: call_once ในบริบทนั้น แต่ด้วยโซลูชันนี้คุณต้องประกาศ std :: once_flag สำหรับทุกที่ที่คุณใช้ std :: call_once ใช่ไหม
ดา

11
วิธีนี้ใช้งานได้ แต่ไม่ได้มีไว้สำหรับการแก้ปัญหาแบบง่ายๆ แต่แนวคิดนี้มีไว้สำหรับแอปพลิเคชันมัลติเธรด มันมากเกินไปสำหรับบางสิ่งที่เรียบง่ายเพราะมันใช้การซิงโครไนซ์ภายใน - ทั้งหมดนี้เป็นสิ่งที่จะแก้ไขได้ด้วย if ธรรมดา เขาไม่ได้ขอวิธีแก้ปัญหาที่ปลอดภัย
Michael Chourdakis

9
@MichaelChourdakis ฉันเห็นด้วยกับคุณมันเกินความจริง อย่างไรก็ตามคุณควรรู้และโดยเฉพาะอย่างยิ่งการรู้เกี่ยวกับความเป็นไปได้ที่จะแสดงสิ่งที่คุณกำลังทำอยู่ ("รันโค้ดนี้ครั้งเดียว") แทนที่จะซ่อนบางสิ่งไว้เบื้องหลัง if-trickery ที่อ่านได้น้อย
lubgr

18
เอ่อเห็นcall_onceฉันหมายความว่าคุณต้องการโทรหาอะไรสักครั้ง บ้าฉันรู้
Barry

3
@SergeyA เนื่องจากใช้การซิงโครไนซ์ภายใน - ทั้งหมดนี้เป็นสิ่งที่จะแก้ไขได้ด้วย if ธรรมดา เป็นวิธีที่ค่อนข้างใช้สำนวนในการทำอย่างอื่นนอกเหนือจากสิ่งที่ขอ
Lightness Races ใน Orbit

52

C ++ มีพื้นฐานโฟลว์การควบคุมในตัวที่ประกอบด้วย "( before-block; condition; after-block )" อยู่แล้ว:

for (static bool b = true; b; b = false)

หรือแฮ็กเกอร์ แต่สั้นกว่า:

for (static bool b; !b; b = !b)

อย่างไรก็ตามฉันคิดว่าเทคนิคใด ๆ ที่นำเสนอในที่นี้ควรใช้ด้วยความระมัดระวังเนื่องจากยังไม่เป็นเรื่องธรรมดา (แต่?)


1
ฉันชอบตัวเลือกแรก (แม้ว่าจะเหมือนกับหลาย ๆ รูปแบบที่นี่ - มันไม่ปลอดภัยสำหรับเธรดดังนั้นโปรดระวัง) ตัวเลือกที่สองทำให้ฉันรู้สึกหนาวสั่น (อ่านยากขึ้นและอาจถูกเรียกใช้กี่ครั้งก็ได้โดยมีเพียงสองเธรด ... b == false: Thread 1ประเมิน!bและเข้าสู่ลูปThread 2ประเมิน!bและเข้าสู่ลูปThread 1ทำสิ่งต่างๆและออกจากลูปสำหรับการตั้งค่าb == falseเป็น!bเช่นb = true... Thread 2ทำสิ่งต่างๆและออกจาก for loop โดยตั้งค่าb == trueเป็น!bเช่นb = falseอนุญาตให้ทำซ้ำกระบวนการทั้งหมดไปเรื่อย ๆ )
CharonX

3
ฉันคิดว่ามันน่าขันที่หนึ่งในโซลูชั่นที่หรูหราที่สุดในการแก้ไขปัญหาที่บางรหัสควรจะดำเนินการเพียงครั้งเดียวเป็นห่วง +1
ดา

2
ฉันจะหลีกเลี่ยงb=!bมันดูดี แต่จริงๆแล้วคุณต้องการให้ค่าเป็นเท็จดังนั้นจึงb=falseเป็นที่ต้องการ
ยอ

2
โปรดทราบว่าบล็อกที่ได้รับการป้องกันจะทำงานอีกครั้งหากออกจากระบบที่ไม่ใช่ในเครื่อง แม้จะเป็นที่ต้องการ แต่ก็แตกต่างจากแนวทางอื่น ๆ ทั้งหมด
Davis Herring

29

ใน C ++ 17 คุณสามารถเขียน

if (static int i; i == 0 && (i = 1)){

เพื่อหลีกเลี่ยงการเล่นiในตัวห่วง iเริ่มต้นด้วย 0 (ค้ำประกันโดยมาตรฐาน) และการแสดงออกหลังจากที่;ชุดiจะ1เป็นครั้งแรกที่ได้รับการประเมิน

โปรดทราบว่าใน C ++ 11 คุณสามารถทำได้เช่นเดียวกันกับฟังก์ชันแลมบ์ดา

if ([]{static int i; return i == 0 && (i = 1);}()){

ซึ่งยังมีข้อได้เปรียบเล็กน้อยที่iไม่รั่วไหลเข้าสู่ตัวห่วง


4
ฉันเสียใจที่จะพูด - ถ้าจะใส่ #define ที่เรียกว่า CALL_ONCE หรือมากกว่านั้นก็จะอ่านได้มากขึ้น
nada

10
ในขณะที่static int i;อาจ (ฉันไม่แน่ใจจริงๆ) เป็นหนึ่งในกรณีที่iรับประกันว่าจะเริ่มต้นใช้งาน0ได้ชัดเจนกว่าstatic int i = 0;ที่นี่
Kyle Willmon

8
ไม่ว่าฉันจะเห็นด้วยกับผู้เริ่มต้นก็เป็นความคิดที่ดีสำหรับการทำความเข้าใจ
Lightness Races in Orbit

5
@Bathsheba Kyle ไม่ได้ดังนั้นการยืนยันของคุณจึงได้รับการพิสูจน์แล้วว่าเป็นเท็จ การเพิ่มอักขระสองตัวมีค่าใช้จ่ายอะไรเพื่อประโยชน์ในการเคลียร์โค้ด คุณเป็น "หัวหน้าสถาปนิกซอฟต์แวร์" คุณควรรู้สิ่งนี้ :)
Lightness Races in Orbit

6
หากคุณคิดว่าการสะกดค่าเริ่มต้นสำหรับตัวแปรนั้นตรงกันข้ามกับความชัดเจนหรือชี้ให้เห็นว่า "มีบางอย่างที่น่าสนใจเกิดขึ้น" ฉันไม่คิดว่าคุณจะสามารถช่วยได้)
Lightness Races ใน Orbit

14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

โซลูชันนี้ปลอดภัยสำหรับเธรด (ไม่เหมือนกับคำแนะนำอื่น ๆ )


3
คุณรู้หรือไม่ว่า()เป็นทางเลือก (ถ้าว่าง) ในการประกาศแลมบ์ดา
Nonyme

9

คุณสามารถรวมการกระทำเพียงครั้งเดียวในตัวสร้างของวัตถุคงที่ที่คุณสร้างอินสแตนซ์แทนเงื่อนไข

ตัวอย่าง:

#include <iostream>
#include <functional>

struct do_once {
    do_once(std::function<void(void)> fun) {
        fun();
    }
};

int main()
{
    for (int i = 0; i < 3; ++i) {
        static do_once action([](){ std::cout << "once\n"; });
        std::cout << "Hello World\n";
    }
}

หรือคุณอาจติดมาโครซึ่งอาจมีลักษณะดังนี้:

#include <iostream>

#define DO_ONCE(exp) \
do { \
  static bool used_before = false; \
  if (used_before) break; \
  used_before = true; \
  { exp; } \
} while(0)  

int main()
{
    for (int i = 0; i < 3; ++i) {
        DO_ONCE(std::cout << "once\n");
        std::cout << "Hello World\n";
    }
}

8

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

if (static int n_times = 3; n_times && n_times--)
{
    std::cout << "Hello world x3" << std::endl;
} 

การแปลสิ่งนี้เป็นกระดาษห่อหุ้มแฟนซีของ @ Acorn จะมีลักษณะดังนี้:

struct n_times {
    int n;
    n_times(int number) {
        n = number;
    };
    explicit operator bool() {
        return n && n--;
    };
};

...

if(static n_times _(2); _)
{
    std::cout << "Hello world twice" << std::endl;
}

7

แม้ว่าการใช้std::exchangeตามที่แนะนำโดย @Acorn อาจเป็นวิธีที่เข้าใจง่ายที่สุด แต่การแลกเปลี่ยนก็ไม่จำเป็นต้องถูกเสมอไป แม้ว่าแน่นอนว่าการเริ่มต้นแบบคงที่จะได้รับการรับรองว่าปลอดภัยสำหรับเธรด (เว้นแต่คุณจะบอกคอมไพเลอร์ของคุณว่าอย่าทำ) ดังนั้นการพิจารณาใด ๆ เกี่ยวกับประสิทธิภาพจึงค่อนข้างไร้ประโยชน์เมื่อมีstaticคีย์เวิร์ด

หากคุณกังวลเกี่ยวกับการเพิ่มประสิทธิภาพแบบไมโคร (เช่นเดียวกับคนที่ใช้ C ++ มักจะเป็น) คุณสามารถขูดboolและใช้intแทนได้เช่นกันซึ่งจะช่วยให้คุณสามารถใช้การเพิ่มหลังการลด (หรือเป็นการเพิ่มขึ้นได้ซึ่งแตกต่างจากการboolลดลงและintจะไม่อิ่มตัวเป็นศูนย์ ... ):

if(static int do_once = 0; !do_once++)

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

โบนัส: คุณสามารถนำไปใช้do_twiceหรือdo_thriceในทำนองเดียวกัน ...


ฉันทดสอบสิ่งนี้และมันยิงหลายครั้งยกเว้นครั้งแรก
ดา

@nada: โง่ฉันคุณพูดถูก ... มันเคยทำงานboolและลดลงครั้งหนึ่ง แต่การเพิ่มขึ้นใช้ได้ดีกับint. ดูการสาธิตออนไลน์: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon

1
สิ่งนี้ยังคงมีปัญหาที่อาจถูกเรียกใช้งานหลายครั้งที่do_onceล้อมรอบและในที่สุดก็จะตี 0 อีกครั้ง (และอีกครั้งและอีกครั้ง ... )
ดา

เพื่อให้แม่นยำยิ่งขึ้น: ตอนนี้จะดำเนินการทุก INT_MAX ครั้ง
ดา

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

4

จากคำตอบที่ยอดเยี่ยมของ @ Bathsheba สำหรับสิ่งนี้ทำให้ง่ายยิ่งขึ้น

ในC++ 17คุณสามารถทำได้:

if (static int i; !i++) {
  cout << "Execute once";
}

(ในเวอร์ชันก่อนหน้านี้เพียงแค่ประกาศint iนอกบล็อกนอกจากนี้ยังใช้งานได้ใน C :))

พูดง่ายๆคือคุณประกาศ i ซึ่งใช้ค่าเริ่มต้นเป็นศูนย์ ( 0) ศูนย์เป็นเท็จดังนั้นเราจึงใช้เครื่องหมายอัศเจรีย์ ( !) ตัวดำเนินการเพื่อลบล้างมัน จากนั้นเราจะพิจารณาคุณสมบัติการเพิ่มขึ้นของตัว<ID>++ดำเนินการซึ่งจะได้รับการประมวลผล (กำหนด ฯลฯ ) ก่อนแล้วจึงเพิ่มขึ้น

ดังนั้นในบล็อกนี้ฉันจะเริ่มต้นและมีค่า 0เพียงครั้งเดียวเมื่อดำเนินการบล็อกแล้วค่าจะเพิ่มขึ้น เราเพียงแค่ใช้ตัว!ดำเนินการเพื่อลบล้างมัน


1
หากสิ่งนี้ถูกโพสต์ไว้ก่อนหน้านี้ตอนนี้อาจเป็นคำตอบที่ยอมรับได้ ยอดเยี่ยมขอบคุณ!
ดา
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.