Подтвердить что ты не робот

Когда и зачем вы используете static с constexpr?

Как отказ от ответственности, я сделал свое исследование по этому поводу, прежде чем спрашивать. Я нашел аналогичный вопрос SO, но ответ там чувствует себя немного "соломой" и на самом деле не ответил на меня лично. Я также ссылался на свою удобную cppreference, но это не дает очень" ошарашенного" объяснения вещей в большинстве случаев.

В принципе, я все еще нажимаю constexpr, но на данный момент я понимаю, что для компиляции требуются выражения. Поскольку они могут существовать только во время компиляции, на самом деле они не будут иметь адрес памяти во время выполнения. Поэтому, когда я вижу, что люди, использующие static constexpr (например, в классе), меня смущает... static здесь будет лишним, так как это полезно только для контекстов выполнения.

Я видел противоречие в выражении "constexpr не допускает ничего, кроме выражения времени компиляции" (особенно здесь, в SO). Тем не менее, статья со страницы Bjarne Stroustrup объясняет в различных примерах, что на самом деле constexpr действительно требует оценки выражения во время компиляции. Если нет, следует создать ошибку компилятора.

Мой предыдущий абзац выглядит немного не по теме, но это базовый уровень, необходимый для понимания того, почему static можно или нужно использовать с constexpr. К сожалению, эта базовая линия имеет много противоречащей информации, которая распространяется вокруг.

Может ли кто-нибудь помочь мне объединить всю эту информацию в чистые факты с примерами и концепциями, которые имеют смысл? В основном вместе с пониманием того, как constexpr действительно ведет себя, почему вы используете static с ним? И с помощью каких областей/сценариев static constexpr имеет смысл, если их можно использовать вместе?

4b9b3361

Ответ 1

Константные переменные не являются значениями времени компиляции

Значение является неизменным и не занимает память (у него нет адреса), однако объекты, объявленные как constexpr, могут быть изменяемыми и занимать память (в соответствии с правилом as-if).

Изменчивость

Большинство объектов, объявленных как constexpr, неизменяемы, но можно определить объект constexpr, который (частично) изменен следующим образом:

struct S {
    mutable int m;
};

int main() {
    constexpr S s{42};
    int arr[s.m];       // error: s.m is not a constant expression
    s.m = 21;           // ok, assigning to a mutable member of a const object
}

Хранение

В соответствии с правилом as-if компилятор может не выделять какое-либо хранилище для хранения значения объекта, объявленного как constexpr. Точно так же он может выполнять такую ​​оптимизацию для переменных, отличных от constexpr. Однако рассмотрим случай, когда нам нужно передать адрес объекта функции, которая не указана; например:

struct data {
    int i;
    double d;
    // some more members
};
int my_algorithm(data const*, int);

int main() {
    constexpr data precomputed = /*...*/;
    int const i = /*run-time value*/;
    my_algorithm(&precomputed, i);
}

Компилятор здесь должен выделить хранилище для precomputed, чтобы передать свой адрес некоторой не-встроенной функции. Компилятор может выделять хранилище для precomputed и i смежно; можно представить ситуации, когда это может повлиять на производительность (см. ниже).

Standardese

Переменные - это либо объекты, либо ссылки [basic]/6. Позвольте сосредоточиться на объектах.

Объявление типа constexpr int a = 42; является грамотно простым объявлением; он состоит из decl-specifier-seq init-declarator-list ;

Из [dcl.dcl]/9 мы можем заключить (но не строго), что такое объявление объявляет объект. В частности, мы можем (строго) заключить, что это объявление объекта, но это включает декларации ссылок. См. Также обсуждение можем ли мы иметь переменные типа void.

constexpr в объявлении объекта подразумевает, что тип объекта const [dcl.constexpr]/9. Объектом является область хранения [intro.object]/1. Мы можем заключить из [intro.object]/6 и [intro.memory]/1, что каждый объект имеет адрес. Обратите внимание, что мы не можем напрямую принимать этот адрес, например. если объект ссылается через prvalue. (Есть даже prvalues, которые не являются объектами, такими как буквальный 42.) Два разных полных объекта должны иметь разные адреса [intro.object]/6.

С этой точки мы можем заключить, что объект, объявленный как constexpr, должен иметь уникальный адрес относительно любой другой (полный) объект.

Кроме того, мы можем заключить, что объявление constexpr int a = 42; объявляет объект с уникальным адресом.

static и constexpr

ИМХО только интересной проблемой является "per-function static", à la

void foo() {
    static constexpr int i = 42;
}

Насколько мне известно, но это кажется еще не совсем понятным - компилятор может вычислить инициализатор переменной constexpr во время выполнения. Но это кажется патологическим; пусть предположим, что этого не делает, то есть он предварительно вычисляет инициализатор во время компиляции.

Инициализация локальной переменной static constexpr выполняется во время статической инициализации, который должен выполняться перед любой динамической инициализацией [basic.start.init]/2. Хотя это не гарантировано, мы можем предположить, что это не налагает затраты времени выполнения/нагрузки. Кроме того, поскольку для постоянной инициализации нет проблем concurrency Я думаю, мы можем с уверенностью предположить, что это не требует поточно-безопасной проверки времени проверки того, была ли уже инициализирована переменная static. (Взгляд в источники clang и gcc должен пролить свет на эти проблемы.)

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

void non_inlined_function(int const*);

void recurse(int const i) {
    constexpr int c = 42;
    // a different address is guaranteed for `c` for each recursion step
    non_inlined_function(&i);
    if(i > 0) recurse(i-1);
}

int main() {
    int i;
    std::cin >> i;
    recurse(i);
}

Заключение

Похоже, что в некоторых случаях можно использовать статический период хранения переменной static constexpr. Однако мы можем потерять локальность этой локальной переменной, как показано в разделе "Хранение" этого ответа. Пока я не увижу контрольный показатель, который показывает, что это реальный эффект, Я предполагаю, что это не имеет значения.

Если есть только эти два эффекта static на объектах constexpr Я использовал бы static по умолчанию: Обычно нам не нужна гарантия уникальных адресов для наших объектов constexpr.

Для изменяемых объектов constexpr (типы классов с членами mutable), существует очевидная разная семантика между локальными static и нестатическими объектами constexpr. Аналогично, если значение самого адреса релевантно (например, для поиска хэш-карты).

Ответ 2

Только примеры. Сообщество wiki.

static == per-function (статическая продолжительность хранения)

Объекты, объявленные как constexpr, имеют адреса, как и любой другой объект. Если по какой-либо причине используется адрес объекта, компилятору, возможно, придется выделить для него хранилище:

constexpr int expensive_computation(int n); // defined elsewhere

void foo(int const p = 3) {
    constexpr static int bar = expensive_computation(42);
    std::cout << static_cast<void const*>(&bar) << "\n";
    if(p) foo(p-1);
}

Адрес переменной будет одинаковым для всех вызовов; для каждого вызова функции для него не потребуется никакого пространства стека. Сравнить с:

void foo(int const p = 3) {
    constexpr int foo = expensive_computation(42);
    std::cout << static_cast<void const*>(&bar) << "\n";
    if(p) foo(p-1);
}

Здесь адреса будут отличаться для каждого (рекурсивного) вызова foo.

Это имеет значение, например, если объект большой (например, массив), и нам нужно использовать его в контексте, где требуется постоянное выражение (требуется постоянная времени компиляции), и нам нужно принять его адрес.

Обратите внимание, что поскольку адреса должны отличаться, объект может быть инициализирован во время выполнения; например, если глубина рекурсии зависит от параметра времени выполнения. Инициализатор все еще может быть предварительно вычислен, но результат может быть скопирован в новую область памяти для каждого этапа рекурсии. В этом случае constexpr гарантирует, что intializer может быть оценен во время компиляции, и инициализация может быть выполнена во время компиляции для переменной этого типа.

static == per-class

template<int N>
struct foo
{
    static constexpr int n = N;
};

То же, что и всегда: объявляет переменную для каждой специализированной специализации (экземпляра) шаблона foo, например. foo<1>, foo<42>, foo<1729>. Если вы хотите открыть параметр шаблона непигового типа, вы можете использовать, например. статический член данных. Это может быть constexpr, так что другие могут извлечь выгоду из значения, известного во время компиляции.

static == внутренняя связь

// namespace-scope
static constexpr int x = 42;

Довольно много избыточности; constexpr переменные имеют внутреннюю привязку по умолчанию. В настоящее время я не вижу причин использовать static.

Ответ 3

Я использую static constexpr в качестве замены неназванных перечислений в местах, где я не знаю точного определения типа, но хочу запросить некоторую информацию о типе (обычно во время компиляции).

Есть некоторые дополнительные преимущества для времени переименования без имени. Более легкая отладка (значения отображаются в отладчике как "нормальная" переменная. Также вы можете использовать любой тип, который может быть сконструирован constexpr (а не только числа), а не просто номера с перечислением.

Примеры:

template<size_t item_count, size_t item_size> struct item_information
{
    static constexpr size_t count_ = item_count;
    static constexpr size_t size_ = item_size;
};

Теперь вы можете получить доступ к этим переменным во время компиляции:

using t = item_information <5, 10>;
constexpr size_t total = t::count_ * t::size_;

Альтернатива:

template<size_t item_count, size_t item_size> struct item_information
{
    enum { count_ = item_count };
    enum { size_ = item_size };
};

template<size_t item_count, size_t item_size> struct item_information
{
    static const size_t count_ = item_count;
    static const size_t size_ = item_size;
};

Альтернативы не имеют всех положительных значений static constexpr - вам гарантирована обработка времени компиляции, безопасность типов и (потенциально) более низкое использование памяти (переменные constexpr не требуют захвата памяти, они эффективно жестко закодированы, если это возможно).

Если вы не начнете принимать адрес переменных constexpr (и, возможно, даже если вы все еще это делаете), у ваших классов не будет увеличения размера, как вы видели со стандартным статическим const.