tisdag 9 juni 2009

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

Detta är del 3 i inläggsserien om att gå från .NET/Java till C++. Förra delen handlade om Boost, det bästa tillägget till standard C++ där ute. Denna del ska handla om kodkonventioner. C++ lider av problemet att det är extremt fritt, och det är lätt att göra subtila felsteg som skapar stora problem senare.

Detta inlägg presenterar därför 9 stycken konventioner man bör följa då man programmerar C++. Alla dessa är tagna från den fantastiska boken "C++ Coding Standards" av Sutter & Alexandrescu, som innehåller 101 "best practices", med lite modifierade exempel och mycket mindre text.

Jag har valt ut de 9 jag finner mest intressanta och användbara, men utelämnat de mest basala (som t ex, att man alltid ska skriva include guards, något som de flesta IDE:er sköter åt automatiskt en nuförtiden, eller att man ska minimera global data).

Använd const proaktivt (punkt 15)
Jag har tidigare pratat om på hur många sätt const kan användas. Även om det är jobbigt att tänka efter precis när man bör ha en const-deklaration skapar det mycket robust kod, speciellt då man skriver SDKer och APIer för andra programmerare.

const är framförallt viktigt att använda för parametrar och returvärden då man hanterar pekare och referenser, och som regel ska man alltid returnera ett const pekare om man vill att anroparen inte ska kunna ändra den.

const har förmågan att propagera genom koden; om man deklarerar const för en medlem eller funktion slutar det ofta att man måste deklarera anropade parametrar och funktioner const. Detta är dock en feature.

Exempel. Det rätta sättet att skicka in en sträng (som inte ska ändras) till en funktion är:

void func(const std::string &str);

Vi deklarerar den som en referens för att slippa onödig kopiering, men som const för att försäkra oss att ingen modifikation av strängen sker.

Minimera cykliska beroenden (punk 22)
Det är väldigt lätt att råka skriva kod där två header-filer inkluderar varanndra, antingen direkt eller genom en cirkel. Detta tyder ofta på att designen i koden är dålig, och även om kompilatorn kan klara av detta (vilket vissa kanske tycker är direkt fel) så avråds man från detta.

Ändock värre är om man hamnar i ett cykliskt beroende med libs eller dller. Detta är extremt dålig design, då ett bibliotek ska symbolisera en oberoende modul, och två bibliotek som beror på varanndra betyder att de egentligen är en enda modul.

Ta parametrar som värden, referenser eller pekare på rätt sätt (punkt 25)
Om man är ny till C++ kan det vara komplicerat att på direkten se hur man bör skicka parametrar till funktioner. Ovan visades ett exempel med const och referens. Sutter, Alexandrescus har några tips:
  • const:a alltid pekare och referenser till input-parametrar
  • Ta värden främst som primitiva typer och sådana typer som är billiga att kopiera
  • Skicka som referens om funktionen inte tänker spara en pekare till argumentet.
Föredra en medlem istället för arv (punkt 34)
Jag tycker att denna punkt är väldigt intressant då jag ser många som använder arv alldelles för flitigt. I många fall är det bättre att bädda in en medlem av en klass i en annan klass (som privat medlem) istället för att skapa en klasshirearki via arv. Du har mycket bättre kontroll över hur klassen kallas, du får kortare kompileringstider, och du får inte lika nära koppling.

Man ska t ex komma ihåg att många klasser inte alls är tänkta att ärva från, speciellt gäller detta nästan alla klasser i std.

Deklarera basklassers destruktorer public och virtual, eller protected och icke-virtuella (punkt 50)
Detta är en ganska subtil punkt som kan orsaka väldigt konstiga runtimefel om man glömmer den. Säg att man deklarerar klasses Base:s destruktor public. I klassen Derived som ärver från Base deklarerar man ingen destruktor. I följande kod:


Derived *d = new Derived();
Base *b = (Base*)d;
delete b;


kommer vi att få ett odefinierat beteende, då Base desturktor anropas, men då b egenligen pekar till en objekt av Derived.

Om Base:s destruktor hade varit virtuell, hade istället Derived:s destruktor kallats, vilket är beteendet vi är ute efter.

Tillåt eller förbjud kopiering explicit (punkt 53)
C++ kompilatorer lägger automatiskt till en copy constructor (och en copy assignment constructor) om man själv inte definierar någon. Detta är ofta bra, men det finns många klasser som aldrig någonsin är tänkta att kopieras. Exempelvis finns det ingen vettig mening med att kopiera en klass som representerar en nätverksförbindelse (som ju binder upp en systemresurs).

För att inte andra ska göra misstaget att råka kopiera din klass är det bäst att explicit förbjuda detta. Det finns olika sätt, men jag föredrar att låta klassen som inte ska kunna kopieras ärva (privat) från boost::noncopyable. Ett annat sätt är att deklarera copy konstruktorerna privat.

Kasta som värde, fånga som referens (punkt 73)
Detta är standardsättet att kasta och fånga undantag på i C++, något som inte är lätt att veta som nybörjare. För det mesta ska man också fånga som const:

try
{
if (fails)
throw MyException();
}
catch (const MyException &e)
{
cout << "Problem"; }


Använd std::vector och std::string::c_str() för att prata med icke-C++ APIer (punkt 78)
För att kommunicera med externa APIer, skrivna i t ex C, behöver man inte transformera alla sina std::vector till arrayer, som många tror. En std::vector fungerar från C sett precis som en array, där första elementet pekar till början på std::vector. Och en std::string som man kallar c_str() på returnerar en C-lik null-terminerad char*.

Föredra algoritmanrop framför handskrivna loopar (punk 84)
Det är lätt att av gammal vana skriva en egen loop för att göra en operation på en kontainer. Dock så finns det massor med fina funktioner i STL och i samarbete med boost::bind kan man skriva många operationer som one-liners, med mycket lägre risk för fel.

Ett exempel: Jag ville nyligen hitta ett object av typen Exercise i en vector där dess medlem std::string name matchade ett namn jag ville söka på, name. Detta går ju att skriva för hand, men denna lösning blir mycket snyggare och säkrare:


std::vector::iterator it = std::find_if(exercises.begin(), exercises.end(), boost::bind(&ExerciseManager::compare, _1, name));


Och i samma klass definieras compare-funktionen:


bool ExerciseManager::compare(Exercise ex, const std::string &name)
{
if (name.compare(ex.name))
return false;
else
return true;
}

Efter anropet kommer iteratorn it (magiskt?) att peka på första elementet med matchande namn. Läs på om STL:s sök - och sorteringsalgoritmer och om boost::bind, om du inte redan gjort det!