Вычисление длины строки C во время компиляции. Это действительно constexpr?

94

Я пытаюсь вычислить длину строкового литерала во время компиляции. Для этого я использую следующий код:

#include <cstdio>

int constexpr length(const char* str)
{
    return *str ? 1 + length(str + 1) : 0;
}

int main()
{
    printf("%d %d", length("abcd"), length("abcdefgh"));
}

Все работает как положено, программа выводит 4 и 8. Ассемблерный код, сгенерированный clang, показывает, что результаты вычисляются во время компиляции:

0x100000f5e:  leaq   0x35(%rip), %rdi          ; "%d %d"
0x100000f65:  movl   $0x4, %esi
0x100000f6a:  movl   $0x8, %edx
0x100000f6f:  xorl   %eax, %eax
0x100000f71:  callq  0x100000f7a               ; symbol stub for: printf

Мой вопрос: гарантировано ли стандартом, что lengthфункция будет оцениваться во время компиляции?

Если это правда, дверь для вычислений строковых литералов времени компиляции только что открылась для меня ... например, я могу вычислять хэши во время компиляции и многое другое ...

Мирча Испас
источник
3
Пока параметр является постоянным выражением, так и должно быть.
Крис
1
@chris Есть ли гарантия, что что-то, что может быть константным выражением, должно быть оценено во время компиляции при использовании в контексте, который не требует постоянного выражения?
TC
12
Кстати, в том числе <cstdio>и звонки ::printfнепереносимы. Стандарт требует только <cstdio>предоставить std::printf.
Ben Voigt
1
@BenVoigt Хорошо, спасибо, что указали на это :) Изначально я использовал std :: cout, но сгенерированный код был довольно большим, чтобы найти фактические значения :)
Мирча Испас
3
@Felics Я часто использую Godbolt, отвечая на вопросы, связанные с оптимизацией, и использование printfможет привести к значительно меньшему количеству кода, с которым нужно работать.
Шафик Ягмур

Ответы:

76

Не гарантируется, что постоянные выражения будут вычислены во время компиляции, у нас есть только ненормативная цитата из чернового стандартного раздела C ++ 5.19 Постоянные выражения, в котором говорится следующее:

[...]> [Примечание: постоянные выражения можно оценивать во время перевода. — конец примечания]

Вы можете присвоить результат constexprпеременной, чтобы убедиться, что он оценивается во время компиляции, мы можем видеть это из справочника Бьярна Страуструпа по C ++ 11, в котором говорится ( выделено мной ):

Помимо возможности оценивать выражения во время компиляции, мы хотим иметь возможность требовать вычисления выражений во время компиляции; constexpr перед определением переменной делает это (и подразумевает const):

Например:

constexpr int len1 = length("abcd") ;

Бьярн Страуструп кратко описывает, когда мы можем гарантировать оценку времени компиляции в этой записи блога isocpp, и говорит:

[...] Правильный ответ - как указано Хербом - заключается в том, что согласно стандарту функция constexpr может оцениваться во время компиляции или во время выполнения, если она не используется как постоянное выражение, и в этом случае она должна быть оценена при компиляции -время. Чтобы гарантировать оценку во время компиляции, мы должны либо использовать его там, где требуется постоянное выражение (например, как привязка массива или как метка case), либо использовать его для инициализации constexpr. Я надеюсь, что ни один уважающий себя компилятор не упустит возможность оптимизации сделать то, что я изначально сказал: «Функция constexpr оценивается во время компиляции, если все ее аргументы являются постоянными выражениями».

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

  1. Используйте его там, где требуется постоянное выражение, это может быть где-нибудь в черновике стандарта, где используется фраза shall be ... converted constant expressionили shall be ... constant expression, например, привязка к массиву.
  2. Используйте его для инициализации a, constexprкак я обрисовал выше.
Шафик Ягмур
источник
4
Тем не менее, в принципе компилятор имеет право видеть объект с внутренней связью или без связи с ним constexpr int x = 5;, обратите внимание, что он не требует значения во время компиляции (при условии, что он не используется в качестве параметра шаблона или еще чего-то) и фактически генерирует код, который вычисляет начальное значение во время выполнения, используя 5 непосредственных значений 1 и 4 операций сложения. Более реалистичный пример: компилятор может достичь предела рекурсии и отложить вычисления до времени выполнения. Если вы не сделаете что-то, что заставляет компилятор фактически использовать значение, «гарантированная оценка во время компиляции» является проблемой QOI.
Стив Джессоп
@SteveJessop Bjarne, похоже, использует концепцию, не имеющую аналога, который я могу найти в черновике стандарта, который используется как средство постоянного выражения, оцениваемое при переводе. Таким образом, может показаться, что в стандарте явно не указано, что он говорит, поэтому я склонен согласиться с вами. Хотя и Бьярн, и Херб, похоже, согласны с этим, что может указывать на то, что он просто недооценен.
Шафик Ягмур
2
Я думаю, что они оба рассматривают только «уважающие себя компиляторы», в отличие от совместимого со стандартами, но умышленно мешающего компилятору, как я предполагаю. Это полезно как средство рассуждений о том, что на самом деле гарантирует стандарт , и не более того ;-)
Стив Джессоп
3
@SteveJessop Намеренно мешающие компиляторы, такие как печально известный (и, к сожалению, несуществующий) Hell ++. Это действительно было бы здорово для проверки совместимости / переносимости.
Энгью больше не гордится SO
Согласно правилу as-if, даже использования значения в качестве кажущейся константы времени компиляции недостаточно: компилятор может отправить копию вашего исходного кода и перекомпилировать ее во время выполнения или произвести расчет рутинных операций, чтобы определить тип переменная, или просто бессмысленно перезапустить свой constexprрасчет из чистого зла. Можно даже подождать 1 секунду для каждого символа в заданной строке исходного текста или взять заданную строку исходного текста и использовать ее для заполнения шахматной позиции, а затем сыграть обеими сторонами, чтобы определить, кто победил.
Якк - Адам Неврамонт
27

Очень легко узнать, приводит ли вызов constexprфункции к основному константному выражению или просто оптимизируется:

Используйте его в контексте, где требуется постоянное выражение.

int main()
{
    constexpr int test_const = length("abcd");
    std::array<char,length("abcdefgh")> test_const2;
}
Бен Фойгт
источник
4
... и скомпилировать -pedantic, если вы используете gcc. В противном случае вы не получите предупреждений и ошибок
BЈовић
@ BЈовић Или используйте его в контексте, где GCC не имеет потенциально мешающих расширений, таких как аргумент шаблона.
Энгью больше не гордится SO
Разве взлом enum не был бы более надежным? Такие как enum { Whatever = length("str") }?
Sharptooth
18
Заслуживает упоминанияstatic_assert(length("str") == 3, "");
Крис
8
constexpr auto test = /*...*/;вероятно, самый общий и простой.
TC
19

Просто обратите внимание, что современные компиляторы (например, gcc-4.x) работают strlenсо строковыми литералами во время компиляции, потому что они обычно определяются как встроенные функции . Без включенной оптимизации. Хотя результат не является постоянной времени компиляции.

Например:

printf("%zu\n", strlen("abc"));

Результаты в:

movl    $3, %esi    # strlen("abc")
movl    $.LC0, %edi # "%zu\n"
movl    $0, %eax
call    printf
Максим Егорушкин
источник
Обратите внимание, это работает, потому что strlenэто встроенная функция, если мы используем -fno-builtinsее, она возвращается к вызову во время выполнения, посмотрите вживую
Шафик Ягмур
strlenэто constexprдля меня, даже с -fno-nonansi-builtins(кажется, -fno-builtinsэто в граммах не существует ++ больше). Я говорю "constexpr", потому что я могу это сделать, template<int> void foo();и foo<strlen("hi")>(); g ++ - 4.8.4
Аарон МакДэйд
19

Позвольте предложить другую функцию, которая вычисляет длину строки во время компиляции без рекурсии.

template< size_t N >
constexpr size_t length( char const (&)[N] )
{
  return N-1;
}

Взгляните на этот пример кода на ideone .

user2436830
источник
5
Он не может быть равен strlen из-за встроенного '\ 0': strlen ("hi \ 0there")! =
Length
Это правильный путь, это пример в Effective Modern C ++ (если я правильно помню). Однако есть хороший строковый класс, который полностью constexpr, см. Этот ответ: str_const Скотта Шурра , возможно, это будет более полезно (и меньше стиля C).
QuantumKarl
@MikeWeir Ops, это странно. Вот различные ссылки: ссылка на вопрос , ссылка на статью , ссылка на источник на git
QuantumKarl
теперь yow do: char temp[256]; sprintf(temp, "%u", 2); if(1 != length(temp)) printf("Your solution doesn't work"); ideone.com/IfKUHV
Пабло Ариэль
7

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

Я использовал следующий трюк, чтобы принудительно выполнить оценку во время компиляции. К сожалению, он работает только с целыми значениями (т.е. не со значениями с плавающей запятой).

template<typename T, T V>
struct static_eval
{
  static constexpr T value = V;
};

Теперь, если вы напишете

if (static_eval<int, length("hello, world")>::value > 7) { ... }

вы можете быть уверены, что этот ifоператор является константой времени компиляции без дополнительных затрат времени выполнения.

5gon12eder
источник
8
или просто используйте std :: integration_constant <int, length (...)> :: value
Мирча Испас
1
Этот пример немного бессмысленен, так lenкак constexprсредства lengthв любом случае должны оцениваться во время компиляции.
Крис
@ Крис , я не знаю , что это должно быть, хотя я заметил , что это с моим компилятором.
5gon12eder
Хорошо, согласно большинству других ответов, это должно быть так, поэтому я изменил пример, чтобы он был менее бессмысленным. Фактически, это было if-условие (где было необходимо, чтобы компилятор выполнял удаление мертвого кода), для которого я изначально использовал этот трюк.
5gon12eder
1

Краткое объяснение из статьи Википедии об обобщенных постоянных выражениях :

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

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

Kaedinger
источник
Эти условия не гарантируют, что возвращаемое значение будет постоянным . Например, функция может быть вызвана с другими значениями аргументов.
Ben Voigt
Верно, @BenVoigt. Я отредактировал его для вызова с постоянным выражением.
Kaedinger