Artykuł “Co nowego w świecie języka C?” z Programisty 68

Dzięki uprzejmości redakcji Programisty, mogę podzielić się tym artykułem.

Co nowego w świecie języka C

C18 – tak najprawdopodobniej będzie brzmiała nazwa nowego standardu języka C. Aby tak się stało, komisja standaryzacyjna musi zakończyć nad nim prace na tyle prędko, aby organizacja ISO mogła go przyjąć jeszcze w w tym roku. W tym artykule opisano wybrane zmiany spośród tych, które mają być wprowadzone.

Brak rozgłosu spowodowany jest tym, że jest to wydanie mające na celu wyłącznie poprawę błędów z poprzednich wersji.
Większość modyfikacji zwiększa precyzję zapisów w samym dokumencie, cofa nieintencjonalne rozmijanie się ze standardem C++ w częściach wspólnych dla obu języków lub usuwa błędy.

Doprecyzowanie inicjalizacji za pomocą designated initializers

C99 wprowadziło możliwość inicjalizacji elementów struktur, unii i tablic w kolejności określonej przez programistę. Dla unii i struktur inicjalizacja odbywa się za pomocą zapisu:

.element = wartość_inicjalizująca

a dla tablic:

[index] = wartość inicjalizująca

Ich przykładowe użycie znajduje się w Listingu 1.

Listing 1. Przykładowe użycie designated initializers

#include <assert.h>
#include <string.h>
 
typedef struct
{
    int num;
    double dbl;
    char const* str;
} foo;
 
int main(void)
{
    foo bar = { .str = "str!", .dbl = 42 };
    assert(bar.num == 0);
    assert(bar.dbl == 42);
    assert(strcmp(bar.str, "str!") == 0);
 
    int arr[3] = { [1] = 42 };
    assert(arr[0] == 0);
    assert(arr[1] == 42);
    assert(arr[2] == 0);
}

Przy takiej inicjalizacji wszystkie niewymienione jawnie elementy inicjalizowane są wartością zero. W bardziej zawiłym przykładzie przedstawionym w Listingu 2. sytuacja nie jest już tak oczywista. Czy wartość l.t.k to 42 (od inicjalizacji .t = x), czy może 0 (od niejawnej inicjalizacji .t podczas inicjalizacji .t.l)?

Listing 2. Przykład z raportu błędu (ang. Defect Report) [1]

typedef struct {
    int k;
    int l;
    int a[2];
} T;
 
typedef struct {
    int i;
    T t;
} S;
 
T x = {.l = 43, .k = 42, .a[1] = 19, .a[0] = 18 };
 
void f(void)
{
    S l = { 1, .t = x, .t.l = 41, .t.a[1] = 17};
}

Wedle komisji uważna lektura standardu pozwala wywnioskować, że jest to 42 – jawna inicjalizacja jest ważniejsza od niejawnej. Pomimo tego odpowiedni paragraf zostanie wzbogacony o przykład wyjaśniający tę wątpliwość.

#elif z niepoprawnym warunkiem

Zamierzeniem twórców języka była ekwiwalencja dyrektywy preprocesora #elif (Listing 3) z dyrektywą #else zawierającą wewnętrzną dyrektywę #if (Listing 4).

Listing 3. Wewnętrzna dyrektywa #if

#if 1
#else
#if EXPR
#endif
#endif
int main(void)
{
}

Listing 4. Dyrektywa #elif analogiczna do #if z Listingu 3

#if 1
#elif EXPR
#endif
int main(void)
{
}

Jeśli jednak EXPR będzie niepoprawnym wyrażeniem, zapis z Listingu 4 powinien spowodować błąd kompilacji – co przedstawiono w Listingu 5.

Listing 5. Program niepoprawny z powodu błędnego warunku w pominiętej ścieżce [2]

#if 1
#elif "
#endif
int main(void)
{
}

Od C18 oba warianty będą miały identyczne znaczenie – wyrażenie #elif będzie miało semantykę zdefiniowaną tak samo jak wyrażenie wewnętrznej dyrektywy #if.

Konwersja liczb zespolonych na _Bool

Zapisy poprzedniego standardu stały ze sobą w sprzeczności. Dla kodu z Listingu 6 jednocześnie aplikowalne są dwa zapisy standardu:

  • o konwersji do _Bool – wartość po konwersji jest równa 0, jeśli wartość konwertowana jest równa zero, w przeciwnym wypadku wartość po konwersji jest równa 1,
  • o konwersji z liczb zespolonych do typów rzeczywistych – część urojona jest odrzucana, a część rzeczywista konwertowana do typu docelowego według normalnych reguł.

Listing 6. Jaka powinna być wartość foo?

#include <complex.h>
#include <stdbool.h>
 
int main()
{
    _Bool foo = 0. + I;
}

Według pierwszego z wymienionych zapisów foo powinno mieć wartość 1, a według drugiego – 0. Autor nie był w stanie znaleźć wersji kompilatorów clang i gcc, które przejawiałyby drugie zachowanie. W C18 drugi z przytoczonych fragmentów o konwersji liczb zespolonych nie będzie dotyczył konwersji do _Bool, czyli foo będzie miało gwarantowaną wartość 1.

Porzucenie ATOMIC_VAR_INIT

Wprowadzone w C11 obiekty atomic musiały być inicjalizowane za pomocą makra ATOMIC_VAR_INIT (Listing 7), inaczej ich wartość była traktowana jako niezdeterminowana1.

Listing 7. Inicjalizacja atomic_int z użyciem ATOMIC_VAR_INIT

#include <assert.h>
#include <stdatomic.h>
#include <threads.h>
 
int change(void* ptr)
{
    *(atomic_int*)ptr = 0;
    return 0;
}
 
int main(void)
{
    atomic_int x = ATOMIC_VAR_INIT(42);
    thrd_t thread;
    thrd_create(&thread, &change, &x);
    while(x);
    assert(x == 0);
    thrd_join(thread, NULL);
}

Ponieważ uniemożliwiało to użycie designated initializers, a popularne implementacje i tak implementowały to makro jako no-op2, zdecydowano się na porzucenie konieczności inicjalizacji w ten sposób. Od C18 legalna i poprawna będzie inicjalizacja przedstawiona w Listingu 8.

Listing 8. Inicjalizacja atomic_int jak zwykłej zmiennej int

#include <assert.h>
#include <stdatomic.h>
#include <threads.h>
 
int change(void* ptr)
{
    *(atomic_int*)ptr = 0;
    return 0;
}
 
int main(void)
{
    atomic_int x = 42;
    thrd_t thread;
    thrd_create(&thread, &change, &x);
    while(x);
    assert(x == 0);
    thrd_join(thread, NULL);
}

Atomowy żart

Wbrew deklaracjom purystów językowych, w tym autora tego artykułu, C oraz C++ mają dużą część wspólną. Twórcy obu języków starają się też nie wprowadzać zbędnych rozbieżności między nimi. Dlatego też, jeśli jakieś wyrażenie pojawi się w opisie części wspólnej w jednym ze standardów, to z bardzo dużym prawdopodobieństwem zbliżone stwierdzenia można będzie również znaleźć w drugim.

Szkic standardu C++11, znany wtedy pod roboczą nazwą C++0x, zawierał, pośród innych, następujące gry słowne:

  • Atomic objects are neither active nor radioactive – obiekty atomic nie są ani aktywne, ani radioaktywne,
  • Among other implications, atomic variables shall not decay – pośród innych implikacji zmienne atomic nie powinny się rozpadać (ang. decay).

O ile pierwsze zdanie jest oczywistym żartem i nie zostało przeniesione do C11, to drugie może wyglądać jak typowe stwierdzenie, które można znaleźć w dokumencie standaryzacyjnym. Logicznie nie ma ono jednak sensu – decay oznacza w C niejawną konwersję typu tablicowego we wskaźnik na element. Pomimo tego znalazło się ono w C11. W C18 usunięto ten żart.

Podsumowanie

Choć w artykule przedstawiono wybrane najbardziej znaczące zmiany, to i tak można je traktować co najwyżej jako drobnostki. Gdyby nie kronikarskie zapędy autora, artykuł o C18 – o ile standard ten będzie ostatecznie nosił taką nazwę – mógłby nigdy nie powstać.

Bibliografia:
[1]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2148.htm#dr_413
[2]: https://wandbox.org/permlink/gdTcEwoGxkNj8rsr

1Nie dotyczy obiektów statycznych i globalnych.
2Pusta instrukcja, brak jakichkolwiek działań. W tym przypadku oznacza to, że istniejące implementacje i tak implementowały makro ATOMIC_VAR_INIT jako ewaluujące się do swojego argumentu.

10 thoughts on “Artykuł “Co nowego w świecie języka C?” z Programisty 68

  1. Szkoda – liczyłem na jakieś większe zmiany ale C chyba nie lubi rewolucji. Lubię C, ale jak Go będzie się rozwijać, to C odejdzie u mnie w niepamięć. Wywalczyłem sobie już jeden projekt na studiach, u zagorzałego fana C, w Golangu i nie straciłem na wydajności, mimo GC*.

    *chociaż może bardziej właściwym byłoby określenie, że otrzymałem wystarczającą wydajność do aplikacji działającej na Cortexie.

    1. Generalną zasadą jest dobieranie najlepszego narzędzia do zadania, jeśli wydajność pozwala to nie ma co się męczyć z relatywnie nieskopoziomowymi językami. Chociaż jako bezpośrednią konkurencję dla C widziałbym C++/D/Rust, a nie Go.

      1. Lubię Rusta – to znaczy nie mam żadnego doświadczenia w pisaniu, ale podoba mi się koncepcja. Natomiast wrogiem dla niego jest u mnie fakt, że może być niskopoziomowym Haskellem, a nie ukrywam, że na obecnym etapie drogi zawodowej, kieruję się również tym jak wygląda rynek.

        Na chwilę obecną Go jest co raz częściej widywane przy IoT, Rusta widuję głównie przy znienawidzonym przeze mnie sektorze – webdevie. 🙁

        1. Nie chcę przewidywać przyszłości, bo nie jestem w tym specjalnie dobry, ale Rust ma za sobą trochę zalet i ma wsparcie dużych korporacji. Powątpiewam, że wygryzie z rynku C – przynajmniej w pełni, ponieważ nie wszystko da się w nim napisać (citation needed, iirc nie da się w bezpiecznym kodzie robić wydajnego przetwarzania audio/video, szczegółów nie znam) – ale nie wiem czy nie przejmie dominacji nad rynkiem niskopoziomowym. Potencjał jest, sam też chętniej widziałbym nowoczesny język zamiast C.

          Co do zastosowań to mnie zaskoczyłeś. Nie robiłem researchu, ale byłem przekonany, że to Go jest właśnie często używane w webie (może jako backend dla serwisów, ale w webie mimo wszystko), a Rust to zamiennik C, czyli IoT/embedded/inne lowlvl raczej.

          1. Dwie oferty w Polsce, które widziałem dla Rusta to Webdev[Wrocław i Łódź]. 🙁

            Go dominuje w Cloudzie, ale co raz więcej ofert widzę w IoT + Robotyce.

          2. To mnie trochę dziwi, biorąc pod uwagę założenia obu języków. No cóż, niezbadane są wyroki biznesu 🙂

          3. “nie da się w bezpiecznym kodzie robić wydajnego przetwarzania audio/video” W bezpiecznym się da, w niemutowalnym nie da. Scyzoryk jest niebezpieczny, ale zależy w czyich rękach. Zamiast C to może https://ziglang.org/ ?

          4. @Aer w IoT i robotyce? Fail. Binarki Go są kobylaste bo kompilator ładuje tam trzeba czy nie trzeba całą bibliotekę standardową, ale być może można to jakoś wyłączyć. Skompiluj jednak na domyślnych ustawieniach “hello word” w Go i porównaj rozmiar binarki z C/C++/Nim/Rust.

    1. Myślę, że na razie za szybko na takie deklaracje, mam za sobą kilka, może kilkanaście godzin z Rustem. Zresztą to nie jest tak, że używałbym wyłącznie jednego języka. Ale widzę potencjał Rusta na to, aby stał się moim głównym. Być może.

Leave a Reply

Your email address will not be published.