Include Guards

Šis straipsniukas originaliai buvo post'as GameDev.lt forume. Post'ą galima rasti čia.


Tiems, kas skaitė šitą (Kopiją taip pat perkėliau ir į puslapį: Deklaracijos ir Definicijos) ir sužinojo būdą kaip sėkmingai į headerius iškelti informaciją apie jų globalius kintamuosius nenuliūdinant linkerio, galbūt iškilo klausimas kaip gi ten su tais... #ifdef kažkokiais.

Taigi šįkart apie juos, apie "include guard'us". Paaiškinsiu kas tai yra, kam to reikia, kodėl tai neturi nieko bendro su globaliais kintamaisiais ir su kuo gi tada turi.

Kas tai yra?

Tai yra makrosų pagalba realizuotas būdas užtikrinti, kad vienas ir tas pats headeris į vieną ir tą patį translation unit'ą pakliūna tik vieną kartą. Štai kaip jie atrodo, tikriausiai ne kartą teko matyti:
// Header1.h

#ifndef HEADER1_DEFINED
#define HEADER1_DEFINED

// The Contents

#endif      // #ifndef HEADER1_DEFINED

Su kuo tai susiję?

Nenustebkite, bet tai susiję su deklaracijomis ir definicijomis :-). Tik šįkart ne su kintamųjų, o su tipų. Čia aš paaiškinau kaip atrodo ir kuo skiriasi kintamojo deklaracija ir definicija. Ten pat paminėjau ir kaip atrodo tipo deklaracija:

struct Foo;

Toks užrašas reiškia tą patį "kompiliatoriaus ir tipo supažindinimą". Radęs nuorodą į Foo (t.y. pointerį arba reference'ą), kompiliatorius nepyks ir žinos, kad toks tipas kažkur yra.

Tuo tarpu kai kompiliatorius kur nors ras bandymą instancijuoti to tipo kintamąjį (t.y. sukurti kintamąjį, kuris yra Foo tipo), jis, jei pamenate, išskirs atmintį. O tam, kad galėtų tai padaryti, kompiliatorius turi ne tik žinoti, kad kažkur yra toks tipas Foo, bet ir tiksliai žinoti koks tai tipas, kiek jis užima atminties. Jis turi matyti tipo definiciją, arba kitaip tariant pilną aprašymą:

struct Foo
{
    int     member;
    float   otherMember;
};

Taip pat kompiliatorius turi matyti pilną tipo aprašymą, jeigu kur nors randa kreipinį į tipą sudarančius narius, net jeigu pats tipas referuojamas per nuorodą:

int foo (Foo const* pFoo)
{
    return pFoo->member + 1;    // compiler still needs to know type definition
                                // in order to access type members
}

Kaip ir su kintamaisiais, tipus galima deklaruoti daug kartų:

struct Foo;
struct Foo;
struct Foo;

Kompiliatorius nepyks, jam nesunku keletą kartų įsiminti tą patį. Tuo tarpu, jeigu kompiliatorius daugiau negu vieną kartą pamatys pilną tipo aprašymą tokiu pačiu pavadinimu, jis negalės nuspręsti, kuris iš jų tikras.

Kaip taip gali būti? Aš nevadinu tipų vienodais vardais!

Labai paprastai. Taip nutinka netgi nevadinant tipų vienodais vardais. Žiū:

// Vector.h
struct Vector
{
    // ...
};
// Plane.h
#include "Vector.h"     // plane needs vectors

struct Plane
{
    Vector  normal;
    // ...
};
// Line.h
#include "Vector.h"     // line also needs vectors

struct Line
{
    Vector  direction;
    // ...
};
// Code.cpp
#include "Vector.h"
#include "Line.h"
#include "Plane.h"

Vector linePlaneIntersection (Line const& line, Plane const& plane)
{
    // ...
}

Dar prieš kompiliuojant (žr. transliavimo stadijas), preprocesorius padaro daug jam skirto darbo. Vienas iš jo darbų yra vietoj kiekvienos #include direktyvos įterpti tą failą, kuris yra include'inamas. Kompiliuojant Code.cpp, pirmiausia vietoj #include "Vector.h" atsiduria visas pastarojo failo turinys. Toliau tas pats padaroma su Line.h. Tačiau pats Line.h taip pat #include'ina Vector.h! Teksto įterpimo procedūra yra pakartojama! Tokiu būdu struct Vector jau yra aprašyta du kartus. Pakartojus tą patį su Plane.h, gaunasi, kad iki kol kompiliatorius pamato kodą Code.cpp, struct Vector ten būna aprašytas net tris kartus. Kompiliatorius pamato "type redefinition" ir barasi.

Kaip tai veikia?

Kaip include guard'ai išsprendžia šią problemą? Ganėtinai paprastai: #ifndef FOO reiškia "if not defined FOO", arba kitaip tariant, ar niekada transliavimo metu nebuvo štai tokios preprocesoriaus direktyvos:

#define FOO

Jeigu nebuvo (t.y. jeigu šis headeris pirmą kartą pakliūna į translation unit'ą), visas jo kodas iki atitinkamo #endif yra įterpiamas. Kartu yra įterpiamas ir #define FOO, kad antrą kartą sąlyga nebūtų patenkinta.

Priešingu atveju, preprocesorius pašalina viską kas yra tarp #ifndef ir #endif, tad kompiliatoriaus headerio turinys nepasiekia. Būtent taip ir įvyksta pakartotinio #include'inimo metu.

Alternatyvos

Kartais galite pamatyti išplėstą #ifndef formą:

#if !defined (FOO)
#define FOO

// ...

#endif

Toks užrašymas reiškia lygiai tą patį, naudokite tą formą, kuri jums gražesnė.

Taip pat kai kurie Microsoft kompiliatoriaus sugadinti žmonės naudoja

#pragma once

Šis užrašas naudojamas vieną kartą headerio viršuje, jis trumpesnis ir daro maždaug tą patį. Tačiau jį naudoti nepatartina. Jis nestandartinis ir veikia tik su kai kuriais kompiliatoriais. Čia jį pateikiu tik supažindinimo tikslais, kad nebūtų naujiena kada nors pamačius.

End of Story

Tai tiek apie include guard'us. Nesitikėjau, kad bus ką tiek daug apie juos rašyti, bet kai pabandai kažką lyg ir savaime suprantamo aprašyti tiksliai ir su smulkmenomis, taip ir gaunasi :-).

-rtfb

Valid HTML 4.01 Strict