วิธีที่เป็นนามธรรมเพื่อแยกแยะคอนสตรัคเตอร์ zero-arg สองตัว


41

ฉันมีชั้นเรียนเช่นนี้:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

โดยปกติฉันต้องการเริ่มต้น (ศูนย์) เริ่มต้นcountsอาร์เรย์ตามที่แสดง

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

อะไรคือวิธีที่ใช้สำนวนและมีประสิทธิภาพในการสร้างตัวสร้าง zero-arg แบบ "รอง" ดังกล่าว

ขณะนี้ฉันใช้คลาสแท็กuninit_tagที่ส่งผ่านเป็นอาร์กิวเมนต์ดัมมี่เช่น:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

จากนั้นฉันเรียกผู้สร้างที่ไม่เริ่มต้นเช่นevent_counts c(uninit_tag{});เมื่อฉันต้องการที่จะปราบปรามการก่อสร้าง

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


"เพราะฉันรู้ว่าอาร์เรย์กำลังจะถูกเขียนทับ" คุณแน่ใจหรือไม่ 100% นักแปลว่าคอมไพเลอร์ของคุณไม่ได้ทำการปรับให้เหมาะสมสำหรับคุณแล้ว? กรณีตรงประเด็น: gcc.godbolt.org/z/bJnAuJ
Frank

6
@ Frank - ฉันรู้สึกว่าคำตอบสำหรับคำถามของคุณอยู่ในช่วงครึ่งหลังของประโยคที่คุณยกมา? มันไม่ได้อยู่ในคำถาม แต่สิ่งต่าง ๆ สามารถเกิดขึ้นได้: (a) บ่อยครั้งที่คอมไพเลอร์ไม่แข็งแรงพอที่จะกำจัดร้านค้าที่ตายแล้ว (b) บางครั้งมีเพียงส่วนย่อยขององค์ประกอบที่ถูกเขียนทับและสิ่งนี้เอาชนะ การปรับให้เหมาะสม (แต่มีการอ่านเซ็ตย่อยเดียวกันในภายหลัง) (c) บางครั้งคอมไพเลอร์สามารถทำได้ แต่แพ้เช่นเนื่องจากวิธีการไม่ได้รับการแทรก
BeeOnRope

คุณมีช่างก่อสร้างอื่น ๆ ในชั้นเรียนของคุณหรือไม่
NathanOliver

1
@ Frank - เอ๊ะกรณีของคุณแสดงให้เห็นว่า gcc ไม่ได้กำจัดร้านค้าที่ตายแล้ว? ในความเป็นจริงถ้าคุณทำให้ฉันเดาว่าฉันคิดว่า gcc จะได้รับกรณีที่ง่ายมาก แต่ถ้ามันล้มเหลวที่นี่ลองจินตนาการถึงกรณีที่ซับซ้อนกว่านี้เล็กน้อย!
BeeOnRope

1
@uneven_mark - ใช่ gcc 9.2 ทำได้ที่ -O3 (แต่การเพิ่มประสิทธิภาพนี้ผิดปกติเมื่อเทียบกับ -O2, IME) แต่รุ่นก่อนหน้านี้ไม่ได้ โดยทั่วไปการกำจัดร้านค้าที่ตายแล้วเป็นสิ่งหนึ่ง แต่มันเปราะบางมากและขึ้นอยู่กับข้อแม้ทั่วไปเช่นคอมไพเลอร์ที่สามารถเห็นร้านค้าที่ตายไปในเวลาเดียวกัน ความคิดเห็นของฉันมีความชัดเจนมากขึ้นว่าแฟรงค์พยายามพูดอะไรเพราะเขาพูดว่า "case in point: (godbolt link)" แต่ลิงก์แสดงให้เห็นว่าร้านค้าทั้งสองกำลังดำเนินการอยู่
BeeOnRope

คำตอบ:


33

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


1
ประเด็นหลักที่ฉันมีคือฉันควรจะประกาศuninit_tagรสชาติใหม่ในทุก ๆ ที่ที่ฉันต้องการใช้สำนวนนี้หรือไม่ std::ผมก็หวังว่ามีอะไรบางอย่างเหมือนเช่นชนิดตัวบ่งชี้แล้วบางทีใน
BeeOnRope

9
ไม่มีทางเลือกที่ชัดเจนจากไลบรารีมาตรฐาน ฉันจะไม่กำหนดแท็กใหม่สำหรับทุก ๆ คลาสที่ฉันต้องการคุณสมบัตินี้ - ฉันจะกำหนดno_initแท็กทั้งโครงการและใช้ในทุกคลาสที่ต้องการ
John Zwinck

2
ผมคิดว่าห้องสมุดมาตรฐานมีแท็กลูกผู้ชายสำหรับ iterators ความแตกต่างและสิ่งดังกล่าวและทั้งสองและstd::piecewise_construct_t std::in_place_tดูเหมือนว่าไม่มีเหตุผลที่จะใช้ที่นี่ บางทีคุณอาจต้องการกำหนดออบเจ็กต์โกลบอลที่เป็นประเภทของคุณให้ใช้เสมอดังนั้นคุณไม่จำเป็นต้องใช้เครื่องมือจัดฟันในการเรียก Constructor ทุกครั้ง STL ไม่นี้ด้วยสำหรับstd::piecewise_construct std::piecewise_construct_t
n314159

มันไม่มีประสิทธิภาพเท่าที่จะเป็นไปได้ ในตัวอย่างการเรียก AArch64 แท็กจะต้องมีการจัดสรรสแต็กโดยมีเอฟเฟ็กต์แบบเคาะ (ไม่สามารถโทรแบบหาง ... ): godbolt.org/z/6mSsmq
TLW

1
@TLW เมื่อคุณเพิ่มเนื้อหาให้กับ constructors ไม่มีการจัดสรรสแต็กgodbolt.org/z/vkCD65
R2RT

8

หากร่างกายของตัวสร้างที่ว่างเปล่าก็สามารถละเว้นหรือเริ่มต้น:

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

จากนั้นการเริ่มต้นเริ่มต้น event_counts counts;จะcounts.countsไม่กำหนดค่าเริ่มต้นไว้(การเริ่มต้นเริ่มต้นเป็นแบบไม่มี op ที่นี่) และการเริ่มต้น event_counts counts{};ค่าจะให้ค่าเริ่มต้นcounts.countsได้อย่างมีประสิทธิภาพเติมด้วยศูนย์


3
แต่อีกครั้งคุณต้องจำไว้ว่าให้ใช้การกำหนดค่าเริ่มต้นและ OP ต้องการให้ปลอดภัยโดยค่าเริ่มต้น
doc

@doc ฉันเห็นด้วย นี่ไม่ใช่ทางออกที่แน่นอนสำหรับสิ่งที่ OP ต้องการ แต่การเริ่มต้นนี้เลียนแบบชนิดในตัว สำหรับint i;เรายอมรับว่ามันไม่ได้เป็นศูนย์เริ่มต้น บางทีเราควรยอมรับevent_counts counts;ว่าไม่ได้กำหนดค่าเริ่มต้นเป็นศูนย์และทำให้เป็นevent_counts counts{};ค่าเริ่มต้นใหม่ของเรา
Evg

6

ฉันชอบทางออกของคุณ คุณอาจพิจารณาโครงสร้างซ้อนกันและตัวแปรคงที่ ตัวอย่างเช่น:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};

ด้วยการเรียกตัวแปรคอนสตรัค uninitialized คงอาจจะสะดวกกว่า:

event_counts e(event_counts::uninit);

แน่นอนคุณสามารถแนะนำแมโครเพื่อบันทึกการพิมพ์และทำให้มันมีคุณสมบัติที่เป็นระบบมากขึ้น

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}

3

ฉันคิดว่า enum เป็นตัวเลือกที่ดีกว่าแท็กคลาสหรือบูล คุณไม่จำเป็นต้องผ่านอินสแตนซ์ของ struct และเป็นที่ชัดเจนจากผู้โทรที่ตัวเลือกของคุณได้รับ

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

จากนั้นการสร้างอินสแตนซ์จะมีลักษณะดังนี้:

event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

หรือหากต้องการให้เป็นวิธีการเรียนแท็กยิ่งขึ้นให้ใช้ enum ค่าเดียวแทนคลาสแท็ก:

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};

จากนั้นมีเพียงสองวิธีในการสร้างอินสแตนซ์:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};

ฉันเห็นด้วยกับคุณ: enum ง่ายกว่า แต่บางทีคุณอาจลืมบรรทัดนี้:event_counts() : counts{} {}
น้ำเงิน

@ สีน้ำเงินความตั้งใจของฉันไม่ได้เริ่มต้นcountsโดยไม่มีเงื่อนไข แต่เมื่อINITมีการตั้งค่า
TimK

@ สีน้ำเงินฉันคิดว่าเหตุผลหลักในการเลือกแท็กคลาสไม่ใช่เพื่อให้เกิดความเรียบง่าย แต่เพื่อส่งสัญญาณว่าวัตถุที่ไม่มีการกำหนดค่าเริ่มต้นเป็นพิเศษนั่นคือใช้คุณลักษณะการปรับให้เหมาะสมแทนส่วนอินเตอร์เฟซปกติของคลาส ทั้งสองboolและenumเหมาะสม แต่เราต้องระวังว่าการใช้พารามิเตอร์แทนการโอเวอร์โหลดนั้นมีเฉดสีที่แตกต่างกันบ้าง ในอดีตคุณเห็นได้ชัดว่าวัตถุ parametrize จึงเริ่มต้น / uninitialized ท่าทางกลายเป็นสถานะของมันในขณะที่ผ่านแท็กวัตถุไปยัง ctor เป็นเหมือนขอให้ชั้นเรียนดำเนินการแปลง ดังนั้นมันไม่ใช่ IMO ที่เป็นตัวเลือกในการสร้างประโยค
doc

@TimK แต่ OP ต้องการให้การทำงานเริ่มต้นเป็นการเริ่มต้นของอาร์เรย์ดังนั้นฉันคิดว่าโซลูชันของคุณสำหรับคำถามควรรวมevent_counts() : counts{} {}อยู่ด้วย
สีน้ำเงิน

@bluish ในข้อเสนอแนะดั้งเดิมของฉันcountsจะเริ่มต้นได้โดยstd::fillถ้าNO_INITมีการร้องขอ การเพิ่มนวกรรมิกเริ่มต้นตามที่คุณแนะนำจะทำให้สองวิธีที่แตกต่างกันของการเริ่มต้นเริ่มต้นซึ่งไม่ใช่ความคิดที่ดี std::fillฉันได้เพิ่มวิธีการที่จะหลีกเลี่ยงการใช้อีก
TimK

1

คุณอาจต้องการพิจารณาการกำหนดค่าเริ่มต้นแบบสองเฟสสำหรับคลาสของคุณ:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

ตัวสร้างด้านบนไม่ได้เริ่มต้นอาร์เรย์เป็นศูนย์ ในการตั้งค่าองค์ประกอบของอาร์เรย์ให้เป็นศูนย์คุณต้องเรียกใช้ฟังก์ชันสมาชิกset_zero()หลังจากการสร้าง


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

3
สิ่งนี้จะต้องใช้ความระมัดระวังเป็นพิเศษแต่การใช้ที่คาดว่าจะไม่ได้กำหนดไว้ ดังนั้นจึงเป็นแหล่งพิเศษของข้อบกพร่องที่เกี่ยวข้องกับการแก้ปัญหา OP
วอลนัต

@BeeOnRope หนึ่งสามารถให้std::functionเป็นอาร์กิวเมนต์ตัวสร้างด้วยสิ่งที่คล้ายกับset_zeroเป็นอาร์กิวเมนต์เริ่มต้น จากนั้นคุณจะส่งผ่านฟังก์ชัน lambda หากคุณต้องการอาร์เรย์ที่ไม่มีการกำหนดค่าเริ่มต้น
doc

1

ฉันจะทำเช่นนี้:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

คอมไพเลอร์จะฉลาดพอที่จะข้ามรหัสทั้งหมดเมื่อคุณใช้event_counts(false)และคุณจะต้องพูดว่าคุณหมายถึงอะไรแทนที่จะทำให้ส่วนต่อประสานของคลาสของคุณแปลกไป


8
คุณพูดถูกเรื่องประสิทธิภาพ แต่พารามิเตอร์บูลีนไม่ได้ทำเพื่ออ่านรหัสลูกค้า เมื่อคุณอ่านไปและคุณเห็นคำประกาศevent_counts(false)นั่นหมายความว่าอย่างไร คุณไม่มีความคิดเลยถ้าไม่ย้อนกลับไปดูชื่อของพารามิเตอร์ ดีกว่าอย่างน้อยควรใช้ enum หรือในกรณีนี้คลาส Sentinel / tag ดังแสดงในคำถาม จากนั้นคุณจะได้รับการประกาศมากขึ้นเช่นevent_counts(no_init)ซึ่งชัดเจนสำหรับทุกคนในความหมายของมัน
โคดี้เกรย์

ฉันคิดว่านี่เป็นวิธีแก้ปัญหาที่ดีเช่นกัน คุณสามารถทิ้ง ctor event_counts(bool initCountr = true)เริ่มต้นและค่าเริ่มต้นใช้งาน
doc

นอกจากนี้ ctor ควรมีความชัดเจน
doc

น่าเสียดายที่ปัจจุบัน C ++ ไม่รองรับพารามิเตอร์ที่มีชื่อ แต่เราสามารถใช้boost::parameterและเรียกevent_counts(initCounts = false)ใช้การอ่านได้
phuclv

1
สนุกมาก @doc event_counts(bool initCounts = true)จริง ๆ แล้วเป็นตัวสร้างเริ่มต้นเนื่องจากทุกพารามิเตอร์มีค่าเริ่มต้น ความต้องการคือมันสามารถเรียกใช้ได้โดยไม่ต้องระบุอาร์กิวเมนต์event_counts ec;ไม่สนใจว่ามันเป็นพารามิเตอร์น้อยกว่าหรือใช้ค่าเริ่มต้น
Justin Time - Reinstate Monica

1

ฉันจะใช้คลาสย่อยเพียงเพื่อประหยัดการพิมพ์:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};

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

คุณสามารถสลับการสืบทอดรอบ ๆ และกำหนดevents_count_no_initด้วย Constructor ที่ผิดนัดเช่น Evg ที่แนะนำในคำตอบของพวกเขาและจากนั้นก็events_countเป็นคลาสย่อย:

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};

นี่เป็นแนวคิดที่น่าสนใจ แต่ฉันก็รู้สึกว่าการแนะนำประเภทใหม่จะทำให้เกิดแรงเสียดทาน เช่นเมื่อฉันต้องการยกเลิกการกำหนดค่าเริ่มevent_countsต้นฉันจะต้องการให้เป็นประเภทevent_countไม่ใช่event_count_uninitializedดังนั้นฉันควรจะแบ่งการก่อสร้างอย่างevent_counts c = event_counts_no_init{};ที่ฉันคิดว่าจะประหยัดการพิมพ์ส่วนใหญ่
BeeOnRope

@BeeOnRope ดีสำหรับวัตถุประสงค์ส่วนใหญ่event_count_uninitializedวัตถุเป็นevent_countวัตถุ นั่นคือจุดรวมของการสืบทอดพวกมันไม่ได้แตกต่างกันอย่างสิ้นเชิง
Ross Ridge

เห็นด้วย แต่การถูนั้นใช้ "เพื่อจุดประสงค์ส่วนใหญ่" พวกเขาจะไม่สามารถเปลี่ยนแปลงได้ - เช่นถ้าคุณลองไปดูกำหนดecuที่จะecทำงาน แต่ไม่ใช่วิธีอื่น ๆ รอบ ๆ หรือถ้าคุณใช้ฟังก์ชั่นเทมเพลตมันเป็นประเภทที่แตกต่างกันและจบลงด้วยอินสแตนซ์ที่แตกต่างกันแม้ว่าพฤติกรรมจะจบลงด้วยความเหมือนกัน โดยเฉพาะอย่างยิ่งกับการใช้งานอย่างหนักในautoครั้งนี้สามารถทำให้เกิดความสับสนและสับสนอย่างแน่นอน: ฉันไม่ต้องการวิธีการที่วัตถุเริ่มต้นได้รับการสะท้อนอย่างถาวรในประเภทของวัตถุ
BeeOnRope
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.