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.
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_allocA 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.
void*: a
konverzió nem objektumkezelésISO 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 |
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.
const,
inline és névterekA 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;
}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.
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.
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.
char *p = "abc";, és miért helytelen mégis a
p[0] = 'A';? Mi változik C++20-ban?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.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.cC17-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.
Ö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.
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.
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.