onsdag 1 april 2009

Från hanterad kod till C++, del 2

Detta är del två i inläggsserien "Från hanterad kod till C++". Del 1 handlade främst om pekare och keywordet new, två ganska jobbiga C/C++ begrepp. Denna del handlar om lite olika mekanismer i C++ som jag tycker kan vara förvirrande i början. En exempel är keyword som får helt olika betydelse beroende på i vilket sammanhang de används. Andra exempel är mekanismer som syntaktiskt är lika andra språk men som i C++ fungerar på ett speciellt sätt.

Casting
C++ har ärvt mycket funktionalitet från C som man inte bör använda. En sådan grej är typecasting. Följande kod kompilerar fint i en C++ kompilator:

void func(DerivedClass *d)
{
BaseClass *b;
b = (BaseClass*) d;
}

Detta ser även identiskt ut med Java/.NETs typecasting. Hursomhelst avråds man från att typecasta på detta sätt i C++, i varje fall för klasser och pekare. För grundtyper tycker jag dock att ovanstående s.k C-style casting är helt okej:


double
d = 3.3;

int i = (int) d;

Men för klasser och pekare, som sagt, bör man använda de i C++ fördefinierade operatorerna:

static_cast (expression)
dynamic_cast (expression)
const_cast (expression)
reinterpret_cast (expression)

De två man använder ofta av dessa är static_cast och dynamic_cast. Dessa finns mycket bra beskrivna på MSDN men jag ska presentera dem i sin korthet.

static_cast

static_cast är den "hårdaste" typen av casting; du tvingar kompilatorn att försöka casta mellan de två typerna, utan att någon runtime-kontroll görs om det går. Detta kan vara bra t ex vid casting nedåt i ett klassträd, då du är säkert på att klassen du vill casta även är av typen du castar till. Till exempel:

class B {};
class D : public B {};

void f(B* pb)
{
D* pd2 = static_cast(pb); // du vet att pb är en tidigare nedåt castad instans av D
}

Om du i exemplet ovan skulle råka ha fel (B är inte en istans av D) kan du få ett allvarligt fel, som t ex access violation.

dynamic_cast
dynamic_cast, vilket kanske är det vanligaste sättet att casta i C++, är mer säkert än static_cast. Vid en dymanic_cast sker en runtime-kontroll av att typerna stämmer, och även kompilatorn skulle klaga om man i exemplet ovan bytte ut static_cast mot dynamic_cast. dynamic_cast fungerar dock bara på pekare och referenser.

const
Ett exempel på ett fult keyword i C++ är const, då det får helt olika betydelser beroende på hur det används. Här är fem exempel:

MyClass
{
static const int myConst = 30; // exempel 1

const char* funcOne(int arg); // exempel 2

int funcTwo(const int arg); // exempel 3

int funcThree(int arg) const; // exempel 4

int funcFour(const BigClass &arg); // exempel 5

};

Första exemplet är enkelt, här deklarerar vi en vanlig konstant precis som i Java/.NET.

I exempel 2 används const för att garantera att returtypen är konstant. Om vi försöker ändra den returnerade char* så kommer vi att generera ett kompileringsfel, vilket är praktiskt just då man returnerar pekare och arrayer som man inte kan ändra på hur man vill.

I exempel 3 säger vi till kompilatorn att arg inte får ändras inne i funktionen (detta används oftast för pekare och referenser, som vi annars skulle kunna påverka inne i funktionen)

I exempel 4 säger vi att funktionen i sig aldrig kommer att ändra på någon medlem i MyClass. Detta kan vara bra för att försäkra sig om att ett funktionsanrop enbart är av ”get”-typ. Om vi försöker ändra på medlemar i klassen kommer kompilatorn att klaga.

Exempel 5 är likt exempel 2, men anledningen att const används här är av ett annat skäl. &-tecknet används här för att slippa att BigClass copy-constructor kallas (vilket ju inte sker då man skickar in en referens i funktionen). Programmeraren vill samtidigt förtydliga till de som ska använda funktionen att &-tecknet inte alls betyder att funktionen kommer att ändra arg, och därav const keywordet.

Templates
Motsvarigheten till generics i Java/.NET heter templates i C++ (eller tvärtom egentligen, då C++ var före). Det handlar alltså om att man kan skicka in en vid kompileringstid okänd typ som argument till en funktion eller som medlemsvariabel i en klass. I Java/.NET finns ju också grundtypen object, med vilken nästan samma sak kan uppnås. Dock så rekommenderas man att använda generics då man kan. I C++ finns inte object, så här måste man använda templates.

I Java/.NET används generics nästan uteslutande för listor av olika slag, men i C++ kan man se templates användas lite friskare, och de kan i vissa specifika tillämpningar verkligen bidra till snygg kod. Jag ska inte brodera ut för mycket i detta ämne, då mekanismen är väldigt lik den i Java/.NET. Det som dock kan nämnas är att C++ igen har förvirrat saker och ting genom att man kan deklarera templates på flera olika sätt, med precis samma betydelse. Till exempel är det ingen skillnad på de två följande deklarationerna:

template <class T> func(T arg);
template <typename T> func(T arg);
Som sagt kan templates användas både för enskilda funktioner (som ovan), eller för en hel klass. Man kan även definiera flera template-typer (här nedan för en hel klass):
template <class T, class U>
class MyClass
{
T member1;
U member2;
};

Det man ska tänka på med templates är att även om de syntaktiskt är mycket enkla, så gör kompilatorn en mängd saker bakom kulisserna för att det ska fungera. Detta kan ibland få oväntade effekter för dig som programmerare. Ett exempel är att function templates måste definieras i include-filen (och inte bara deklareras där, alltså) för att du ska kunna kalla funktionen från en annan fil. Detta har att göra med hur kompilatorn "gissar" vilka typer som funktionen ska förvänta sig.

copy constructor
Som jag nämnde i del 1 så används en implicit copy-constructor för att kopiera typer då argument ska slussas in i funktioner och ut som returvärden, samt vid tilldelningar. Undantaget är om man skickar pekare eller referenser. Detta är som sagt förvirrande för oss från Java/.NET då vi är vana vid att man alltid automatiskt skickar referensen till en klass, vad man än gör. Hursomhelst är det så det fungerar i C++, och det är bara att vänja sig.

Det finns möjlighet för en själv att definiera en copy-constructor till ens klass, vilken liknar den vanliga konstruktorn i utseende. Varför vill man göra detta? Svaret är att på vissa klasser duger inte den implicita copy-constructorn. Detta är fallet om en klass innehåller pekare till dynamiskt allokerat minne, alltså då keywordet new har använts i klassen. Vi måste då skriva en egen copy-constructor. Se följande exempel snott från www.codesource.net:

class B // Med copy constructor
{
private:
char *name;

public:
B()
{
name = new char[20];
}

~B()
{
delete name[];
}

// Copy constructor
B(const B &b)
{
name = new char[20];
strcpy(name, b.name);
}
};
name är en dynamiskt allokerad variabel, kommer vi att behöva den explicita konstruktorn för att kopiering av klassen ska utföras korrekt.

Slutsats
Detta var ett litet urval på egenheter med C++. Nästa inlägg kommer att handla om mitt favorittillägg till standard C++, nämligen Boost!

1 kommentar:

  1. Ser fram emot att läsa om Boost..har hört mkt bra om det!

    SvaraRadera