Ідіоматичний спосіб відрізнити два конструктори з нульовим аргументом


41

У мене такий клас:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Зазвичай я хочу, щоб за замовчуванням (нуль) ініціалізувати countsмасив, як показано.

Однак у вибраних місцях, визначених шляхом профілювання, я хотів би придушити ініціалізацію масиву, оскільки я знаю, що масив збирається перезаписати, але компілятор недостатньо розумний, щоб це зрозуміти.

Який ідіоматичний та ефективний спосіб створити такий "вторинний" конструктор з нульовим аргументом?

В даний час я використовую клас тегів, uninit_tagякий передається як фіктивний аргумент, наприклад:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Тоді я називаю конструктор no-init, як event_counts c(uninit_tag{});коли я хочу придушити будівництво.

Я відкритий до рішень, які не передбачають створення фіктивного класу, або якісь ефективніші, тощо.


"тому що я знаю, що масив ось-ось буде перезаписаний" Ви на 100% впевнені, що ваш компілятор вже не робить для вас оптимізацію? справа в пункті: gcc.godbolt.org/z/bJnAuJ
Френк

6
@Frank - я відчуваю, що відповідь на ваше запитання є у другій половині речення, яке ви цитували? Це не належить до питання, але можуть траплятися різні речі: (а) часто компілятор просто недостатньо сильний для усунення мертвих сховищ (b) іноді перезаписується лише підмножина елементів, і це перемагає оптимізація (але згодом зчитується лише той самий підмножина) (c) інколи компілятор міг би це зробити, але зазнає поразки, наприклад, тому що метод не є накресленим.
BeeOnRope

Чи є у вас інші конструктори у вашому класі?
NathanOliver

1
@Frank - е, ваш конкретний випадок показує, що gcc не ліквідує мертві магазини? Насправді, якби ви змусили мене здогадуватися, я б подумав, що gcc виправдає цей дуже простий випадок, але якщо він тут не вдається, то уявіть собі дещо складніший випадок!
BeeOnRope

1
@uneven_mark - так, gcc 9.2 робить це на -O3 (але ця оптимізація є рідкістю порівняно з -O2, IME), але більш ранні версії цього не зробили. Взагалі, усунення мертвих магазинів - це річ, але вона є дуже крихкою і підпорядковується усім звичним застереженням, наприклад, компілятор може бачити мертві сховища в той же час, коли він бачить домінуючі магазини. Мій коментар був більше для уточнення того, що Френк намагався сказати, тому що він сказав "справа в точці: (посилання на боболь)", але посилання показує, що обидва магазини виконуються (тому, можливо, мені щось не вистачає).
BeeOnRope

Відповіді:


33

Рішення, яке ви вже маєте, є правильним, і саме те, що я хотів би побачити, переглядаючи ваш код. Він максимально ефективний, чіткий і стислий.


1
Основне питання, яке я маю, чи повинен я декларувати новий uninit_tagаромат у кожному місці, де я хочу використовувати цю ідіому. Я сподівався, що щось подібне типу індикатора вже є, можливо, в std::.
BeeOnRope

9
Немає очевидного вибору зі стандартної бібліотеки. Я б не визначав новий тег для кожного класу, де я хочу цю функцію - я б визначив тег, що стосується всього проекту, no_initі використовувати його у всіх своїх класах, де це потрібно.
Джон Цвінк

2
Я думаю, що у стандартній бібліотеці є прискіпливі теги для розмежування ітераторів і подібних матеріалів, і двох, std::piecewise_construct_tі std::in_place_t. Жоден з них не здається розумним використовувати тут. Можливо, ви хочете визначити глобальний об'єкт свого типу, який слід використовувати завжди, тому вам не потрібні дужки в кожному виклику конструктора. STL робить це std::piecewise_constructза допомогою std::piecewise_construct_t.
n314159

Це не настільки ефективно, наскільки це можливо. Наприклад, у конвенції про виклик AArch64 тег повинен бути розподілений стеком, з ефектом стукання (не можна також називати хвостик ...): godbolt.org/z/6mSsmq
TLW

1
@TLW Коли ви додасте тіло до конструкторів, не виділяється стек, godbolt.org/z/vkCD65
R2RT

8

Якщо корпус конструктора порожній, його можна опустити або дефолтувати:

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

Тоді ініціалізація за замовчуванням event_counts counts; залишиться counts.countsнеініціалізованою (ініціалізація за замовчуванням тут не працює), а ініціалізація event_counts counts{}; значень буде ініціалізувати значення counts.counts, ефективно заповнюючи її нулями.


3
Але знову ж таки, ви повинні пам’ятати про використання ініціалізації значень, і OP хоче, щоб воно було безпечним за замовчуванням.
док.

@doc, я згоден. Це не точне рішення того, чого хоче ОП. Але ця ініціалізація імітує вбудовані типи. Бо 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

};

За допомогою статичної змінної неініціалізований виклик конструктора може здатися більш зручним:

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

Я думаю, що перелік є кращим вибором, ніж клас тегів чи бул. Вам не потрібно передавати екземпляр структури, і від абонента зрозуміло, який варіант ви отримуєте.

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{} {}
синюватий

@bluish, моїм наміром було не ініціалізувати countsбеззастережно, а лише тоді, коли INITвстановлено.
TimK

@bluish Я думаю, що головна причина вибору класу тегів - не досягнення простоти, а сигналізація про те, що неініціалізований об’єкт є особливим, тобто він використовує функцію оптимізації, а не звичайну частину інтерфейсу класу. І те, boolі enumдруге, але ми маємо пам’ятати, що використання параметра замість перевантаження має дещо інший семантичний відтінок. У першому ви чітко параметризуєте об'єкт, отже, ініціалізована / неініціалізована позиція стає його станом, тоді як передача тегового об'єкта в ctor - це більше як запит класу здійснити перетворення. Тож це не ІМО - питання синтаксичного вибору.
doc

@TimK Але ОП хоче, щоб поведінка за замовчуванням була ініціалізацією масиву, тому я думаю, що ваше рішення питання має включати 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
Це вимагатиме взагалі додаткової обережності, окрім випадків використання, які повинні бути неініціалізовані. Тож це додаткове джерело помилок щодо рішення ОП.
волоський горіх

@BeeOnRope можна також надати std::functionаргумент конструктора з чимось схожим на set_zeroаргумент за замовчуванням. Потім ви передасте лямбда-функцію, якщо хочете неініціалізований масив.
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 або, у цьому випадку, клас дозорних / тегів, як показано у питанні. Потім ви отримуєте декларацію більше подібну 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;не важливо, чи немає параметрів або використовує значення за замовчуванням.
Час Джастіна -

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конструктор за замовчуванням, як 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об'єктом. У цьому вся суть спадщини, вони не зовсім різні типи.
Росс Ридж

Домовились, але руб за допомогою "для більшості цілей". Вони не є взаємозамінними - наприклад, якщо ви намагаєтеся побачити правонаступник ecuна ecце працює, але не навпаки. Або якщо ви використовуєте функції шаблону, вони бувають різних типів і закінчуються різними моментами, навіть якщо поведінка виявляється однаковою (а іноді це не буде, наприклад, зі статичними членами шаблону). Тим більше, що при інтенсивному використанні autoцього виразно можна назріти та заплутати: я б не хотів, щоб ініціалізація об'єкта постійно відображалась у своєму типі.
BeeOnRope
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.