Идиоматический способ различения двух нулевых конструкторов


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

};

Затем я вызываю конструктор без инициализации, например, event_counts c(uninit_tag{});когда я хочу подавить конструкцию.

Я открыт для решений, которые не включают создание фиктивного класса, или более эффективны в некотором роде, и т. Д.


«потому что я знаю, что массив будет перезаписан». Вы на 100% уверены, что ваш компилятор уже не выполняет эту оптимизацию для вас? Показательный пример
Франк

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

У вас есть другие конструкторы в вашем классе?
Натан Оливер

1
@ Фрэнк - да, ваш пример показывает, что gcc не устраняет мертвые хранилища? На самом деле, если бы вы заставили меня догадаться, я бы подумал, что gcc исправит этот очень простой случай, но если он потерпит неудачу, представьте себе немного более сложный случай!
BeeOnRope

1
@uneven_mark - да, gcc 9.2 делает это при -O3 (но эта оптимизация необычна по сравнению с -O2, IME), но более ранние версии этого не сделали. В общем, устранение мертвых хранилищ - это вещь, но она очень хрупкая и подвержена всем обычным предостережениям, таким как то, что компилятор может видеть мертвые хранилища в то же время, когда видит доминирующие хранилища. Мой комментарий был больше для того, чтобы уточнить, что Фрэнк пытался сказать, потому что он сказал: «Показательный пример: (ссылка Godbolt)», но эта ссылка показывает, как работают оба магазина (так что, возможно, я что-то упустил).
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 тег должен быть размещен в стеке с эффектами включения (также не может быть вызван
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

Я думаю, что enum - лучший выбор, чем класс tag или bool. Вам не нужно передавать экземпляр структуры, и из вызывающей стороны ясно, какой вариант вы получаете.

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

Или, чтобы сделать его более похожим на подход класса тегов, используйте перечисление с одним значением вместо класса тега:

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 больше похожа на запрос класса к выполнению преобразования. Так что это не IMO вопрос синтаксического выбора.
док

@TimK Но ОП хочет, чтобы поведение по умолчанию было инициализация массива, поэтому я думаю, что ваше решение вопроса должно включать event_counts() : counts{} {}.
голубоватое

@bluish В моем первоначальном предложении countsинициализируется, std::fillесли не требуется NO_INIT. Добавление конструктора по умолчанию, как вы предлагаете, приведет к двум различным способам инициализации по умолчанию, что не очень хорошая идея. Я добавил другой подход, который избегает использования std::fill.
ТимК

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
Для этого потребуется дополнительный уход должны быть приняты на всех , но виды использования, которые должны быть инициализирован. Таким образом, это дополнительный источник ошибок относительно решения OPs.
грецкий орех

@BeeOnRope можно также предоставить std::functionв качестве аргумента конструктора нечто похожее на set_zeroаргумент по умолчанию. Затем вы должны передать лямбда-функцию, если вы хотите неинициализированный массив.
док

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).
док

Кроме того, ctor должен быть явным.
док

к сожалению, в настоящее время 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.