18.3 C és C++: azonos szintaxis, eltérő szemantika

A C++ fordító nem egyszerűen több hibát kereső C fordító. A két nyelvnek külön típus-, objektum- és élettartam-szabályai vannak: ugyanaz a forrás lehet mindkettőben elfogadható, mégis eltérő jelentésű. A sikeres fordítás ezért szükséges, de nem elégséges kompatibilitási ellenőrzés.

Az összehasonlítás alapja itt ISO C17 és ISO C++20, nem valamely fordító bővítményekkel kiegészített alapértelmezett módja. A C23 könyvtár és preprocessor részben tárgyalt képességellenőrzés továbbra is szükséges; a C23 támogatását nem feltételezzük. Különösen fontos előzmény az effective type és aliasing, valamint a C inline-szabályrendszere.

18.3.1 Külön fordítási mód, külön ellenőrzés

A labor választott célprofilja Linux, x86-64 System V ABI, LP64; GCC 14.2.0 és Clang 19.1.7, glibc 2.40, mindkét fordítóhoz libstdc++ 14.2.0. Ez reprodukciós célprofil, nem itt végrehajtott mérés vagy igazolt telepítés. A példák nem támaszkodnak konkrét objektumelrendezésre.

A következő két teljes program fordítási parancsa:

gcc -std=c17 -Wall -Wextra -Wpedantic -pedantic-errors c_alloc.c -o c_alloc
g++ -std=c++20 -Wall -Wextra -Wpedantic -pedantic-errors cpp_alloc.cpp -o cpp_alloc

A második ellenőrzéshez használjuk ugyanezeket a kapcsolókat clang, illetve clang++ mellett. A további kódok jelölt részletek; önálló fájlba és szükség szerint függvénytörzsbe helyezve vizsgálhatók.

18.3.2 void*: a konverzió nem objektumkezelés

ISO C17: a void* implicit módon konvertálható objektumtípusra mutató pointerré. ISO C++20: ez a visszairányú implicit konverzió nem megengedett. Objektumpointerből void* viszont mindkét nyelvben képezhető, a típusminősítők korlátozásainak megtartásával.

A teljes C17-program:

/* c_alloc.c */
#include <stdlib.h>

int main(void)
{
    int *p = malloc(sizeof *p);
    if (p == NULL) {
        return 1;
    }

    *p = 42;
    int result = (*p == 42) ? 0 : 2;
    free(p);
    return result;
}

A megfelelő C++20-program:

// cpp_alloc.cpp
#include <cstdlib>

int main()
{
    int *p = static_cast<int *>(std::malloc(sizeof(int)));
    if (p == nullptr) {
        return 1;
    }

    *p = 42;
    int result = (*p == 42) ? 0 : 2;
    std::free(p);
    return result;
}

A cast C++-ban a pointerkonverziót teszi explicitté; nem inicializál és nem hív konstruktort. C++20-ban a malloc az implicit élettartam-kezdés szabályai alapján létrehozhatja az itt szükséges int objektumot. Ebből nem következik, hogy tetszőleges osztálypéldány ugyanígy kezelhető.

Helyességi bizonyításvázlat. Sikertelen allokáció után nincs dereferencia. Siker esetén a méret és az igazítás elegendő egy int számára. Az értékadás megelőzi az olvasást; C-ben ez az allokált objektum effective type-ját is meghatározza az érintett hozzáférésekhez. A felszabadítás pontosan egyszer történik.

Szerződési elem Mindkét programban
Tulajdonos A main, amely p segítségével kezeli az allokációt
Tároló A sikeres malloc által biztosított terület
Kölcsönzött hozzáférés Nem kerül kiadásra
Érvénytelenítés free, illetve std::free
Hibakimenet Allokációs hibánál 1; siker esetén várt visszatérés 0

18.3.3 Sztringliterál: eltérő típus, azonos módosítási tilalom

A közönséges "ok" literál C17-ben char[3], C++20-ban const char[3] típusú. A három elem a két karakterből és a lezáró nullkarakterből adódik.

Az alábbi deklarációk függvénytörzsbe illeszthető részletek:

/* C17: elfogadható deklaráció. */
char *s = "ok";

/* Hibás használat: végrehajtása UB. */
/* s[0] = 'O'; */
// C++20: szándékosan hibás deklaráció, diagnosztikát igényel.
char *s = "ok";

C-ben tehát a nem const elemtípus nem jelent módosítási engedélyt. Mindkét nyelvben megfelelő:

/* C17 és C++20 alatt is használható részlet. */
const char *view = "ok";
char text[] = "ok";
text[0] = 'O';

A view nem tulajdonló hozzáférés statikus tárolási idejű literálhoz; a literál tárolója a program végéig él. A text ezzel szemben saját, módosítható tömbobjektum. Függvényblokkon belül deklarálva élettartama a blokk elhagyásakor véget ér; az elemeire korábban képzett pointerek ezután dangling hozzáférést jelentenének.

18.3.4 const, inline és névterek

A kapcsolás (linkage) azt szabályozza, hogy külön deklarációk ugyanazt az entitást jelölhetik-e. Ez nem azonos a hatókörrel.

Deklaráció és környezet C17 C++20
const int limit = 64;, fájl-, illetve névtérszinten, előzmény nélkül Külső kapcsolás Alapesetben belső kapcsolás
static inline int f(void) { return 1; } Fordítási egységenként külön függvény Fordítási egységenként külön függvény
Külső kapcsolású inline függvény Sajátos inline/külső definíciós szabályok Több fordítási egységben definiálható az ODR feltételeivel

A C++-os const sorban fontos az „alapesetben”: például az extern, az inline, illetve egy korábbi deklaráció módosíthatja a kapcsolási helyzetet. Ha egy header egyszerűen const int limit = 64; definíciót tartalmaz, több C fordítási egységben többszörös külső definíció keletkezhet; C++-ban rendszerint külön objektumok jönnek létre.

C17-ben a kizárólag inline, de nem extern fájlszintű deklarációkkal rendelkező függvény inline definíciója nem biztosít külső definíciót. Ha a függvényt használjuk, arról külön gondoskodni kell. A C++ többdefiníciós lehetőségét ezért nem szabad C-re átvinni. Az inline egyik nyelvben sem garantál gépi kódba történő beillesztést.

C-nek vannak külön azonosító-névterei, például a struktúratagek számára, de nincs C++-szerű, felhasználó által megnyitható névtere:

// C++20-részlet; C17-ben nincs megfelelő namespace szintaxis.
namespace diagnostics {
    int status = 0;
}

18.3.5 Union, aliasing és az objektum létrejötte

ISO C17: union másik tagján keresztül olvasva a tárolt reprezentáció újraértelmezése történik. Ez nem általános, hordozható számkonverzió: nem meghatározott reprezentációs bitek és trap reprezentáció is problémát okozhatnak.

ISO C++20: inaktív uniontag értékének olvasása általában UB. Léteznek szűk kivételek, például megfelelő standard-layout struktúrák közös kezdősorozatához kapcsolódóan.

/* Összehasonlító részlet; nem hordozható konverziós recept. */
union sample {
    unsigned int bits;
    float value;
};

union sample s = { .bits = 0 };
float x = s.value;

C-ben a unionra vonatkozó reprezentációs szabályokat kell alkalmazni; C++20-ban az utolsó sor inaktív tagot olvas. Attól, hogy mindkét fordító elfogadja a szintaxist, a művelet még nem lesz mindkét nyelvben helyes.

A C effective type szabályait sem lehet változtatás nélkül C++-ra alkalmazni. Ott az objektumtípus és az objektum-élettartam szabályait kell követni. Egy pointercast önmagában egyik nyelvben sem igazolja az aliasing helyességét.

A különbség konstruktor mellett különösen látható:

// C++20-részlet, függvénytörzsben.
struct Counter {
    int value;
    Counter() : value(7) {}
};

Counter *p = new Counter;
int result = p->value;
delete p;

A new Counter tárolót foglal és konstruktort hív; a delete lezárja az objektum élettartamát és felszabadítja a tárolót. A tulajdonos itt a befoglaló függvény, kölcsönzött pointer nem kerül kiadásra. A szokásos dobó new allokációs hibája kivétel, nem nullpointer.

Ha ezt malloc és cast helyettesítené, a konstruktor nem futna le. A C++20 implicit objektumlétrehozási szabálya erre a konstruktoros típusra nem ad ilyen helyettesítést. C-ben a memóriafoglalás és az értékek beállítása explicit programlépések; C++-ban az inicializálás konstruktorhívást is magában foglalhat.

18.3.6 Designated initializer és compound literal

A kijelölt inicializáló (designated initializer) mindkét választott szabványban létezik, de eltérő korlátozásokkal:

/* C17-részlet: a deklarációs sorrend felcserélhető. */
struct Pair { int first; int second; };
struct Pair p = { .second = 2, .first = 1 };
// C++20-részlet: aggregátum, deklarációs sorrendben.
struct Pair { int first; int second; };
Pair p{ .first = 1, .second = 2 };

C++20-ban a designatoroknak a tagok deklarációs sorrendjét kell követniük. A C tömbindex-designatorai és beágyazott kijelölései sem emelhetők át általánosan C++20-ba.

A C compound literal kifejezéssel névtelen objektum hozható létre:

/* C17-részlet; consume deklarációja előzőleg látható. */
consume((struct Pair){ .first = 1, .second = 2 });

Ez nem ISO C++20-szintaxis. Érték szerint fogadó függvénynél megfelelő C++-alak lehet:

// C++20-részlet.
consume(Pair{1, 2});

Pointert megőrző interfésznél azonban ez nem mechanikus helyettesítés. A blokkban létrehozott C compound literal objektum élettartama a befoglaló blokkhoz kötődik; a bemutatott C++ ideiglenes objektumé a hívást tartalmazó teljes kifejezés végén lezárul.

18.3.7 Gyakori hibák

A legveszélyesebb javítás a „tegyünk castot oda, ahol a C++ fordító panaszkodik”. Ez elrejtheti a valódi objektumélettartam-problémát. Hasonlóan hibás a bővítményként elfogadott compound literalt szabványos C++-nak tekinteni, vagy egyetlen fordítási egység alapján következtetni a header kapcsolási helyességére. A negatív fordítási teszt, a többfájlos linkteszt és a szemantikai ellenőrzés külön feladat.

18.3.8 Feladatok

  1. Magyarázat: miért helyes deklaráció C17-ben a char *p = "abc";, és miért helytelen mégis a p[0] = 'A';? Mi változik C++20-ban?
  2. Kódolás: írj érték szerint visszatérő make_pair függvényt külön C17 és C++20 nyelven. Az eredmény tagjai legyenek 1 és 2; ne használj dinamikus memóriát.
  3. Diagnosztika: mentsd a fordított sorrendű C designated initializer példáját probe.c fájlba. Ellenőrizd mindkét nyelven, majd jósolj meg egy olyan hibát, amelyet ez a vizsgálat nem talál meg.
gcc -x c -std=c17 -Wall -Wextra -pedantic-errors -fsyntax-only probe.c
g++ -x c++ -std=c++20 -Wall -Wextra -pedantic-errors -fsyntax-only probe.c

18.3.9 Megoldások

18.3.9.1 Sztring és módosíthatóság

C17-ben a literál elemtípusa char, így a pointer inicializálása megengedett, de a literál módosítását külön szabály tiltja: végrehajtása UB. C++20-ban már a deklarációhoz is diagnosztika szükséges a const elvesztése miatt. Módosítható tárolóhoz mindkét nyelvben char p[] = "abc"; megfelelő. Az ellenőrzési feltétel a saját tömb és a literál tárolójának megkülönböztetése, nem egy UB-program megfigyelt kimenete.

18.3.9.2 Érték szerint visszatérő függvény

Önálló fordítási egységként, a fenti módokban -c kapcsolóval ellenőrizhető:

/* C17 */
struct Pair { int first; int second; };

struct Pair make_pair(void)
{
    return (struct Pair){ .first = 1, .second = 2 };
}
// C++20
struct Pair { int first; int second; };

Pair make_pair()
{
    return Pair{ .first = 1, .second = 2 };
}

Mindkét függvény értéket ad vissza, nem megszűnő lokális objektum pointerét. A hívó ellenőrizze mindkét tag értékét.

18.3.9.3 Fordítás és szemantika

A C17-vizsgálat várt eredménye elfogadás; C++20-ban a kijelölések sorrendje diagnosztikát igényel. A konkrét üzenet fordítófüggő.

A -fsyntax-only nem végez linkelést, ezért például a több fordítási egységben definiált, külső kapcsolású C const objektum problémáját nem mutatja meg. Az elfogadott uniontag-olvasás helyességét sem bizonyítja.

18.3.10 Összefoglaló

A C és a C++ közötti átjárásnál külön kell ellenőrizni a konverziót, kapcsolást, inicializálást, aliasingot és élettartamot. A két nyelvi módban történő fordítás hasznos első szűrő; a helyességhez azonban a kiválasztott nyelv saját szemantikai szabályai szükségesek.