Cara idiomatik untuk membedakan dua konstruktor zero-arg

41

Saya memiliki kelas seperti ini:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Biasanya saya ingin default (nol) menginisialisasi countsarray seperti yang ditunjukkan.

Di lokasi tertentu yang diidentifikasi oleh profil, bagaimanapun, saya ingin menekan inisialisasi array, karena saya tahu array akan ditimpa, tetapi kompiler tidak cukup pintar untuk mengetahuinya.

Apa cara idiomatis dan efisien untuk membuat konstruktor zero-arg "sekunder" seperti itu?

Saat ini, saya menggunakan kelas tag uninit_tagyang dilewatkan sebagai argumen dummy, seperti:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Lalu saya memanggil konstruktor no-init seperti event_counts c(uninit_tag{});ketika saya ingin menekan konstruksi.

Saya terbuka untuk solusi yang tidak melibatkan pembuatan kelas dummy, atau lebih efisien dalam beberapa cara, dll.

BeeOnRope
sumber
"karena saya tahu array akan ditimpa" Apakah Anda 100% yakin kompiler Anda tidak melakukan optimasi untuk Anda? contoh kasus: gcc.godbolt.org/z/bJnAuJ
Frank
6
@ Jujur - saya merasa jawaban untuk pertanyaan Anda ada di bagian kedua kalimat yang Anda kutip? Itu tidak termasuk dalam pertanyaan, tetapi berbagai hal dapat terjadi: (a) seringkali kompiler tidak cukup kuat untuk menghilangkan toko yang mati (b) kadang-kadang hanya sebagian dari elemen yang ditimpa dan ini mengalahkan optimasi (tetapi hanya subset yang sama kemudian dibaca) (c) kadang-kadang kompiler dapat melakukannya, tetapi dikalahkan misalnya, karena metode ini tidak diuraikan.
BeeOnRope
Apakah Anda memiliki konstruktor lain di kelas Anda?
NathanOliver
1
@ Jujur - eh, kasus Anda menunjukkan bahwa gcc tidak menghilangkan toko mati? Bahkan, jika Anda membuat saya menebak saya akan berpikir bahwa gcc akan menyelesaikan kasus yang sangat sederhana ini, tetapi jika gagal di sini maka bayangkan kasus yang sedikit lebih rumit!
BeeOnRope
1
@uneven_mark - ya, gcc 9,2 melakukannya di -O3 (tapi optimasi ini tidak biasa dibandingkan dengan -O2, IME), tetapi versi sebelumnya tidak. Secara umum, eliminasi toko mati adalah suatu hal, tetapi sangat rapuh dan tunduk pada semua peringatan yang biasa, seperti penyusun yang dapat melihat toko yang mati pada saat yang sama ia melihat toko yang mendominasi. Komentar saya lebih untuk memperjelas apa yang coba dikatakan Frank karena dia mengatakan "case in point: (godbolt link)" tetapi tautan tersebut menunjukkan kedua toko sedang dilakukan (jadi mungkin saya kehilangan sesuatu).
BeeOnRope

Jawaban:

33

Solusi yang sudah Anda miliki sudah benar, dan saya ingin melihat apakah saya sedang meninjau kode Anda. Ini seefisien mungkin, jelas dan ringkas.

John Zwinck
sumber
1
Masalah utama yang saya miliki adalah apakah saya harus menyatakan uninit_tagrasa baru di setiap tempat yang ingin saya gunakan idiom ini. Saya berharap sudah ada semacam indikator seperti itu, mungkin di std::.
BeeOnRope
9
Tidak ada pilihan yang jelas dari perpustakaan standar. Saya tidak akan mendefinisikan tag baru untuk setiap kelas di mana saya menginginkan fitur ini - saya akan mendefinisikan no_inittag proyek-lebar dan menggunakannya di semua kelas saya di mana diperlukan.
John Zwinck
2
Saya pikir perpustakaan standar memiliki tag jantan untuk membedakan iterator dan hal-hal seperti itu dan dua std::piecewise_construct_tdan std::in_place_t. Tak satu pun dari mereka tampaknya masuk akal untuk digunakan di sini. Mungkin Anda ingin selalu mendefinisikan objek global tipe Anda, jadi Anda tidak perlu kawat gigi di setiap panggilan konstruktor. STL melakukan ini std::piecewise_constructuntuk std::piecewise_construct_t.
n314159
Itu tidak seefisien mungkin. Dalam konvensi pemanggilan AArch64 misalnya, tag harus dialokasikan secara stack, dengan efek knock-on (tidak dapat memanggil balik
TLW
1
@ TTW Setelah Anda menambahkan tubuh ke konstruktor tidak ada alokasi tumpukan, godbolt.org/z/vkCD65
R2RT
8

Jika badan konstruktor kosong, itu dapat dihilangkan atau default:

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

Kemudian inisialisasi default event_counts counts; akan meninggalkan inisialisasicounts.counts (inisialisasi default adalah no-op di sini), dan inisialisasi event_counts counts{}; nilai akan nilai inisialisasi counts.counts, secara efektif mengisinya dengan nol.

Evg
sumber
3
Tetapi sekali lagi Anda harus ingat untuk menggunakan inisialisasi nilai dan OP ingin itu aman secara default.
doc
@ doc, saya setuju. Ini bukan solusi tepat untuk apa yang diinginkan OP. Tapi inisialisasi ini meniru tipe bawaan. Karena int i;kami menerima bahwa itu tidak diinisialisasi nol. Mungkin kita juga harus menerima yang event_counts counts;tidak diinisialisasi nol dan menjadikan event_counts counts{};standar baru kita.
Evg
6

Saya suka solusi Anda. Anda mungkin juga mempertimbangkan bersarang struct dan variabel statis. Sebagai contoh:

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

};

Dengan variabel panggilan konstruktor tak diinisialisasi variabel statis mungkin tampak lebih nyaman:

event_counts e(event_counts::uninit);

Tentu saja Anda dapat memperkenalkan makro untuk menyimpan pengetikan dan menjadikannya lebih sebagai fitur yang sistematis

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

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}
dokter
sumber
3

Saya pikir enum adalah pilihan yang lebih baik daripada kelas tag atau bool. Anda tidak perlu melewati instance dari struct dan jelas dari pemanggil opsi mana yang Anda dapatkan.

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

Kemudian membuat instance terlihat seperti ini:

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

Atau, untuk membuatnya lebih seperti pendekatan kelas tag, gunakan enum nilai tunggal alih-alih kelas tag:

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

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

Lalu hanya ada dua cara untuk membuat instance:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};
TIMK
sumber
Saya setuju dengan Anda: enum lebih sederhana. Tapi mungkin Anda lupa baris ini:event_counts() : counts{} {}
kebiruan
@Bluish, maksud saya bukan menginisialisasi countstanpa syarat, tetapi hanya ketika INITdiatur.
TimK
@Bluish Saya pikir alasan utama untuk memilih kelas tag bukan untuk mencapai kesederhanaan, tetapi untuk menandakan bahwa objek yang tidak diinisialisasi adalah khusus, yaitu menggunakan fitur optimasi daripada bagian normal dari antarmuka kelas. Keduanya booldan enumlayak, tetapi kita harus menyadari bahwa menggunakan parameter alih-alih memiliki warna semantik yang agak berbeda. Dalam yang pertama Anda jelas parametrize objek, maka sikap inisialisasi / tidak diinisialisasi menjadi keadaannya, sedangkan meneruskan objek tag ke ctor lebih seperti meminta kelas untuk melakukan konversi. Jadi itu bukan IMO masalah pilihan sintaksis.
doc
@TimK Tapi OP ingin perilaku default menjadi inisialisasi array, jadi saya pikir solusi Anda untuk pertanyaan harus disertakan event_counts() : counts{} {}.
kebiruan
@Bluish Dalam saran asli saya countsdiinisialisasi dengan std::fillkecuali NO_INITdiminta. Menambahkan konstruktor default seperti yang Anda sarankan akan membuat dua cara berbeda dalam melakukan inisialisasi default, yang bukan ide bagus. Saya telah menambahkan pendekatan lain yang menghindari penggunaan std::fill.
TimK
1

Anda mungkin ingin mempertimbangkan inisialisasi dua fase untuk kelas Anda:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

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

Konstruktor di atas tidak menginisialisasi array ke nol. Untuk mengatur elemen array ke nol, Anda harus memanggil fungsi anggota set_zero()setelah konstruksi.

眠 り ネ ロ ク
sumber
7
Terima kasih, saya mempertimbangkan pendekatan ini tetapi menginginkan sesuatu yang menjaga keamanan default - yaitu, nol secara default, dan hanya di beberapa tempat yang dipilih saya mengganti perilaku ke yang tidak aman.
BeeOnRope
3
Ini akan membutuhkan perhatian ekstra untuk diambil sama sekali tetapi penggunaan yang seharusnya tidak diinisialisasi. Jadi itu adalah sumber tambahan bug relatif terhadap solusi OPs.
walnut
@BeeOnRope seseorang juga bisa menyediakan std::functionargumen konstruktor dengan sesuatu yang mirip dengan set_zeroargumen default. Anda kemudian akan melewati fungsi lambda jika Anda ingin array yang tidak diinisialisasi.
doc
1

Saya akan melakukannya seperti ini:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

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

Kompiler akan cukup pintar untuk melewati semua kode saat Anda gunakan event_counts(false), dan Anda bisa mengatakan dengan tepat apa yang Anda maksud alih-alih membuat antarmuka kelas Anda jadi aneh.

Matt Timmermans
sumber
8
Anda benar tentang efisiensi, tetapi parameter boolean tidak menghasilkan kode klien yang dapat dibaca. Ketika Anda membaca bersama, dan Anda melihat deklarasi itu event_counts(false), apa artinya itu? Anda tidak tahu tanpa kembali dan melihat nama parameter. Lebih baik setidaknya menggunakan enum, atau, dalam hal ini, kelas sentinel / tag seperti yang ditunjukkan dalam pertanyaan. Kemudian, Anda mendapatkan deklarasi yang lebih mirip event_counts(no_init),, yang jelas bagi semua orang dalam artinya.
Cody Gray
Saya pikir ini juga solusi yang layak. Anda bisa membuang ctor default dan menggunakan nilai default event_counts(bool initCountr = true).
doc
Juga, aktor harus eksplisit.
Doc
sayangnya saat ini C ++ tidak mendukung parameter bernama, tetapi kita dapat menggunakan boost::parameterdan menyerukan event_counts(initCounts = false)keterbacaan
phuclv
1
Lucunya, @doc, event_counts(bool initCounts = true)sebenarnya adalah konstruktor default, karena setiap parameter memiliki nilai default. Persyaratannya hanyalah callable tanpa menentukan argumen, event_counts ec;tidak peduli apakah itu parameter-kurang atau menggunakan nilai default.
Justin Time - Pasang kembali Monica
1

Saya akan menggunakan subkelas hanya untuk menyimpan sedikit pengetikan:

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

Anda dapat menyingkirkan kelas dummy dengan mengubah argumen konstruktor yang tidak menginisialisasi boolatau intatau sesuatu, karena tidak harus mnemonik lagi.

Anda juga bisa menukar pewarisan sekitar dan mendefinisikan events_count_no_initdengan konstruktor default seperti yang disarankan Evg dalam jawaban mereka, dan kemudian events_countmenjadi subclass:

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{} {}
};
Ross Ridge
sumber
Ini adalah ide yang menarik, tetapi saya juga merasa memperkenalkan tipe baru akan menyebabkan gesekan. Misalnya, ketika saya benar-benar menginginkan yang tidak diinisialisasi event_counts, saya ingin jenisnya event_counttidak event_count_uninitialized, jadi saya harus mengiris tepat pada konstruksi seperti event_counts c = event_counts_no_init{};, yang saya pikir menghilangkan sebagian besar penghematan dalam mengetik.
BeeOnRope
@ BeeOnRope Ya, untuk sebagian besar tujuan, sebuah event_count_uninitializedobjek adalah event_countobjek. Itulah inti warisan, mereka bukan tipe yang sepenuhnya berbeda.
Ross Ridge
Setuju, tetapi intinya adalah "untuk sebagian besar tujuan". Mereka tidak saling dipertukarkan - misalnya, jika Anda mencoba untuk melihat assign ecuuntuk ecbekerja, tapi tidak jalan di sekitar lainnya. Atau jika Anda menggunakan fungsi templat, mereka adalah tipe yang berbeda dan diakhiri dengan instantiasi yang berbeda walaupun perilaku tersebut pada akhirnya identik (dan terkadang tidak seperti, dengan anggota templat statis). Terutama dengan penggunaan yang berat dari autoini pasti dapat muncul dan membingungkan: Saya tidak ingin cara objek diinisialisasi secara permanen tercermin dalam tipenya.
BeeOnRope