Obliczanie długości łańcucha C w czasie kompilacji. Czy to naprawdę constexpr?

94

Próbuję obliczyć długość literału ciągu w czasie kompilacji. Aby to zrobić, używam następującego kodu:

#include <cstdio>

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

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

Wszystko działa zgodnie z oczekiwaniami, program wypisuje 4 i 8. Kod asemblera wygenerowany przez clang pokazuje, że wyniki są obliczane w czasie kompilacji:

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

Moje pytanie: czy standard gwarantuje, że lengthfunkcja będzie oceniana w czasie kompilacji?

Jeśli to prawda, drzwi do obliczeń literałów ciągów w czasie kompilacji właśnie otworzyły się dla mnie ... na przykład mogę obliczyć skróty w czasie kompilacji i wiele więcej ...

Mircea Ispas
źródło
3
Dopóki parametr jest wyrażeniem stałym, musi nim być.
chris
1
@chris Czy istnieje gwarancja, że ​​coś, co może być wyrażeniem stałym, musi być oceniane w czasie kompilacji, gdy jest używane w kontekście, który nie wymaga wyrażenia stałego?
TC
12
BTW, w tym, <cstdio>a następnie dzwonienie ::printfnie jest przenośne. Norma wymaga jedynie <cstdio>podania std::printf.
Ben Voigt
1
@BenVoigt Ok, dzięki za wskazanie tego :) Początkowo użyłem std :: cout, ale wygenerowany kod był dość duży, aby znaleźć rzeczywiste wartości :)
Mircea Ispas
3
@Felics Często używam godbolt , odpowiadając na pytania dotyczące optymalizacji i używania, co printfmoże prowadzić do znacznie mniejszej ilości kodu do obsługi.
Shafik Yaghmour,

Odpowiedzi:

76

Nie ma gwarancji, że wyrażenia stałe zostaną ocenione w czasie kompilacji, mamy tylko nienormatywny cytat z wersji roboczej C ++ sekcji standardowej 5.19 Wyrażenia stałe, który mówi tak:

[...]> [Uwaga: wyrażenia stałe można oceniać podczas tłumaczenia. — uwaga końcowa]

Możesz przypisać wynik do constexprzmiennej, aby upewnić się, że jest ona oceniana w czasie kompilacji, możemy to zobaczyć z referencji C ++ 11 Bjarne Stroustrupa, która mówi ( wyróżnienie moje ):

Oprócz możliwości oceny wyrażeń w czasie kompilacji, chcemy móc wymagać, aby wyrażenia były oceniane w czasie kompilacji; constexpr przed definicją zmiennej robi to (i implikuje const):

Na przykład:

constexpr int len1 = length("abcd") ;

Bjarne Stroustrup podaje podsumowanie, kiedy możemy zapewnić ocenę czasu kompilacji w tym wpisie na blogu isocpp i mówi:

[...] Prawidłową odpowiedzią - jak stwierdził Herb - jest to, że zgodnie ze standardem funkcja constexpr może być oceniana w czasie kompilacji lub w czasie wykonywania, chyba że jest używana jako wyrażenie stałe, w którym to przypadku musi być oceniana podczas kompilacji -czas. Aby zagwarantować ocenę czasu kompilacji, musimy użyć go tam, gdzie wymagane jest stałe wyrażenie (np. Jako powiązana tablica lub jako etykieta przypadku) lub użyć go do zainicjowania constexpr. Mam nadzieję, że żaden szanujący się kompilator nie przegapi okazji do optymalizacji, aby zrobić to, co pierwotnie powiedziałem: „Funkcja constexpr jest oceniana w czasie kompilacji, jeśli wszystkie jej argumenty są wyrażeniami stałymi”.

To przedstawia dwa przypadki, w których powinno się to oceniać w czasie kompilacji:

  1. Użyj go tam, gdzie wymagane jest stałe wyrażenie, wydaje się, że znajduje się ono w dowolnym miejscu w standardowym projekcie, w którym fraza shall be ... converted constant expressionlub shall be ... constant expressionjest używana, na przykład w powiązaniu z tablicą.
  2. Użyj go, aby zainicjować, constexprjak opisałem powyżej.
Shafik Yaghmour
źródło
4
To powiedziawszy, w zasadzie kompilator jest uprawniony do oglądania obiektu z wewnętrznym połączeniem lub bez niego, zauważ constexpr int x = 5;, że nie wymaga wartości w czasie kompilacji (zakładając, że nie jest używany jako parametr szablonu lub cokolwiek innego) i faktycznie emituje kod, który oblicza wartość początkową w czasie wykonywania przy użyciu 5 bezpośrednich wartości 1 i 4 dodatkowych operacji. Bardziej realistyczny przykład: kompilator może osiągnąć limit rekursji i odroczyć obliczenia do czasu wykonania. O ile nie zrobisz czegoś, co zmusi kompilator do rzeczywistego użycia wartości, „gwarantowana ocena w czasie kompilacji” jest problemem QOI.
Steve Jessop
@SteveJessop Bjarne wydaje się używać koncepcji, która nie ma odpowiednika, który mogę znaleźć w projekcie standardu, który jest używany jako stały środek wyrażenia oceniany podczas tłumaczenia. Wydawałoby się więc, że standard nie określa wprost tego, co mówi, więc skłaniałbym się do zgodzenia z tobą. Chociaż wydaje się, że zarówno Bjarne, jak i Herb zgadzają się z tym, co może wskazywać, że jest to po prostu niedookreślone.
Shafik Yaghmour,
2
Myślę, że obaj rozważają tylko „szanujące się kompilatory”, w przeciwieństwie do zgodnych ze standardami, ale świadomie przeszkadzających kompilatorów, o których myślę. Jest to przydatne jako sposób rozumowania tego, co faktycznie gwarantuje norma , i niewiele więcej ;-)
Steve Jessop
3
@SteveJessop Rozmyślnie przeszkadzające kompilatory, takie jak niesławne (i niestety nieistniejące) Hell ++. Taka rzecz byłaby naprawdę świetna do testowania zgodności / przenośności.
Angew nie jest już dumny z SO
Zgodnie z zasadą as-if, nawet użycie wartości jako pozornej stałej czasowej kompilacji nie wystarczy: kompilator może przesłać kopię źródła i ponownie skompilować ją w czasie wykonywania lub wykonać obliczenia ru ti e w celu określenia typu zmienne lub po prostu bezcelowo powtarzaj constexprobliczenia z czystego zła. Można nawet poczekać 1 sekundę na znak w danej linii źródła lub wziąć daną linię źródła i użyć jej do rozstawienia pozycji szachowej, a następnie zagrać w obie strony, aby określić, kto wygrał.
Yakk - Adam Nevraumont
27

Naprawdę łatwo jest dowiedzieć się, czy wywołanie constexprfunkcji powoduje powstanie podstawowego wyrażenia stałego, czy tylko jest optymalizowane:

Użyj go w kontekście, w którym wymagane jest stałe wyrażenie.

int main()
{
    constexpr int test_const = length("abcd");
    std::array<char,length("abcdefgh")> test_const2;
}
Ben Voigt
źródło
4
... i kompiluj z -pedantic, jeśli używasz gcc. W przeciwnym razie nie otrzymasz żadnych ostrzeżeń ani błędów
BЈовић
@ BЈовић Lub użyj go w kontekście, w którym GCC nie ma żadnych rozszerzeń potencjalnie przeszkadzających, takich jak argument szablonu.
Angew nie jest już dumny z SO
Czy hack wyliczenia nie byłby bardziej niezawodny? Takich jak enum { Whatever = length("str") }?
ostry ząb
18
Warte wspomnienia jeststatic_assert(length("str") == 3, "");
chris
8
constexpr auto test = /*...*/;jest prawdopodobnie najbardziej ogólna i prosta.
TC
19

Tylko uwaga, że ​​nowoczesne kompilatory (takie jak gcc-4.x) robią strlendla literałów łańcuchowych w czasie kompilacji, ponieważ jest zwykle definiowana jako funkcja wewnętrzna . Bez włączonej optymalizacji. Chociaż wynik nie jest stałą czasową kompilacji.

Na przykład:

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

Prowadzi do:

movl    $3, %esi    # strlen("abc")
movl    $.LC0, %edi # "%zu\n"
movl    $0, %eax
call    printf
Maxim Egorushkin
źródło
Uwaga, to działa, ponieważ strlenjest to funkcja wbudowana, jeśli użyjemy -fno-builtinsjej , powrócimy do wywoływania jej w czasie wykonywania, zobacz to na żywo
Shafik Yaghmour
strlenjest constexprdla mnie, nawet z -fno-nonansi-builtins(wygląda na to, -fno-builtinsże już nie istnieje w g ++). Mówię „constexpr”, bo mogę to zrobić template<int> void foo();i foo<strlen("hi")>(); g ++ - 4.8.4
Aaron McDaid.
19

Pozwólcie, że zaproponuję inną funkcję, która oblicza długość łańcucha w czasie kompilacji bez rekurencji.

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

Spójrz na ten przykładowy kod w ideone .

user2436830
źródło
5
Nie może być równe strlen z powodu osadzonego '\ 0': strlen ("hi \ 0there")! = Length ("hi \ 0there")
unkulunkulu
To jest poprawny sposób, to jest przykład w Effective Modern C ++ (jeśli dobrze pamiętam). Istnieje jednak ładna klasa string, która jest całkowicie constexpr. Zobacz odpowiedź: str_const Scotta Schurra , być może będzie to bardziej przydatne (i mniej w stylu C).
QuantumKarl
@MikeWeir Ops, to dziwne. Oto różne linki: link do pytania , link do artykułu , link do źródła na git
QuantumKarl
teraz zrób: char temp[256]; sprintf(temp, "%u", 2); if(1 != length(temp)) printf("Your solution doesn't work"); ideone.com/IfKUHV
Pablo Ariel
7

Nie ma gwarancji, że constexprfunkcja jest oceniana w czasie kompilacji, chociaż każdy rozsądny kompilator wykona to na odpowiednich, włączonych poziomach optymalizacji. Z drugiej strony parametry szablonu muszą być oceniane w czasie kompilacji.

Użyłem następującej sztuczki, aby wymusić ocenę w czasie kompilacji. Niestety działa tylko z wartościami całkowitymi (tj. Nie z wartościami zmiennoprzecinkowymi).

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

Teraz, jeśli napiszesz

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

możesz być pewien, że ifinstrukcja jest stałą czasu kompilacji bez narzutu czasu wykonywania.

5gon12eder
źródło
8
lub po prostu użyj std :: integral_constant <int, length (...)> :: value
Mircea Ispas
1
Przykład jest trochę bezcelowy, ponieważ lenbycie constexprśrodkiem i tak lengthmusi zostać ocenione w czasie kompilacji.
chris
@chris Nie wiedziałem, że to musi być, chociaż zauważyłem, że tak jest z moim kompilatorem.
5gon12eder
Ok, zgodnie z większością innych odpowiedzi musi, więc zmodyfikowałem ten przykład, aby był mniej bezcelowy. W rzeczywistości był to ifwarunek -warunek (w którym kompilator usunął martwy kod), dla którego pierwotnie użyłem sztuczki.
5gon12eder
1

Krótkie wyjaśnienie z wpisu Wikipedii o uogólnionych wyrażeniach stałych :

Użycie constexpr na funkcji nakłada pewne ograniczenia na to, co ta funkcja może zrobić. Po pierwsze, funkcja musi mieć zwrotny typ, który nie jest pusty. Po drugie, treść funkcji nie może deklarować zmiennych ani definiować nowych typów. Po trzecie, treść może zawierać tylko deklaracje, instrukcje null i jedną instrukcję powrotu. Muszą istnieć takie wartości argumentów, że po podstawieniu argumentów wyrażenie w instrukcji return tworzy wyrażenie stałe.

Umieszczenie constexprsłowa kluczowego przed definicją funkcji instruuje kompilator, aby sprawdzić, czy te ograniczenia są spełnione. Jeśli tak, a funkcja jest wywoływana ze stałą, gwarantuje się, że zwracana wartość będzie stała, a zatem może być używana wszędzie tam, gdzie wymagane jest wyrażenie stałe.

kaedinger
źródło
Warunki te nie gwarantują, że zwracana wartość jest stała . Na przykład funkcja może zostać wywołana z innymi wartościami argumentów.
Ben Voigt
Racja, @BenVoigt. Zredagowałem go tak, aby był nazywany ciągłym wyrażeniem.
kaedinger