Boost/Älykkäät osoittimet

Boostin älykkäät osoittimet ovat kuuluisin ja käytetyin osa kirjastoa. Boostin shared_ptr alkaa olla käytössä jo joka toisella C++-ohjelmoijalla. Se helpottaa muistinhallintaa tuhoamalla automaattisesti ne oliot, joihin ei viitata. Menetelmä perustuu viitelaskureihin (reference counting). Tämän luvun yhteydessä viite ei tarkoita ainoastaan C++-viitteitä, vaan yleisesti viittausta toiseen olioon erilaisten osoittimien avulla.

Kertausta: std::auto_ptr<T> muokkaa

Moni aloitteleva C++-ohjelmoija ei ole ehkä vielä törmännyt muistin vapauttamiseen liittyviin ongelmiin. On helppo muistaa kirjoittaa delete destruktoriin, kun olion on ensin luonut konstruktorissa:

// kuvitteellinen esimerkki - oikeasti kannattaisi käyttää RAII-idiomin
// mukaisesti automaattisia jäsenmuuttujia eikä varata muistia itse
class Ship {
private:
    Weapon* weapon1, weapon2;
public:
    Ship() {
        weapon1 = new Weapon;
        weapon2 = new Weapon;
    }
    ~Ship() {
        delete weapon2;
        delete weapon1;
    }
...

Kuitenkin jos konstruktorissa sattuu poikkeus, destruktoria ei kutsuta lainkaan! Hieman kokeneempi muistaa, että tällöin täytyy käyttää vakiokirjaston ainoaa älykästä osoitinta, auto_ptr<T>-luokkaa:

#include <memory>
...
class Ship {
private:
    std::auto_ptr<Weapon> weapon1, weapon2;
public:
    Ship() {
        weapon1 = std::auto_ptr<Weapon> (new Weapon);

        // olio tuhotaan ilman destruktorin kutsua
        // jäsenmuuttujat tuhoutuvat normaalisti
        throw std::exception("catch this"); 

        weapon2 = std::auto_ptr<Weapon> (new Weapon);
    }
    ~Ship() {
    }
...

Osoittimen weapon1 viittaama olio tuhotaan auto_ptr-luokan destruktorissa. Koska auto_ptr-oliota käytetään automaattisena- eli pinomuuttujana, destruktoria kutsutaan automaattisesti. Kannattaa muutenkin aina käyttää automaattisia muuttujia jos mahdollista – tätä kutsutaan Resource Acquisition Is Initialization (RAII) -idiomiksi. Harjoituksen vuoksi voit kirjoittaa itse auto_ptrin kaltaisen luokan. Se on helppoa: konstruktori tallettaa osoittimen jäsenmuuttujaan, destruktori tuhoaa sen.

auto_ptr ei tee juuri mitään muuta hyödyllistä. Sen melkoisen turhiin ominaisuuksiin kuuluu ohjelmoijaa hämäävä kopiointisemantiikka: p_new = p_old siirtää tuhoamisvastuun p_new-oliolle ja nollaa p_old-olion, mikä sekoittaa myös säiliöluokat. Itse asiassa Boostissa on scoped_ptr, joka on kuin auto_ptr mutta sitä ei voi kopioida lainkaan. Se selventää lukijalle osoittimen käyttötarkoitusta.

Poikkeukset ovat vasta pieni ongelma: ne johtavat usein koko ohjelman sammumiseen, jolloin muisti vapautuu kuitenkin.

Varsinainen ongelma muokkaa

Todellinen ongelma on se, että olioiden omistussuhteet ovat harvoin tiukan hierarkisia. Otetaan esimerkki: Oliot A ja B käyttävät oliota C, mutta A:n ja B:n tuhoutumisjärjestys ei ole määrätty. Tulisiko C tuhota A:n vai B:n destruktorissa? Tähän ei auto_ptr riitä. Monen C++-kirjaston dokumentaatio onkin täynnä selityksiä siitä, kenen täytyy tuhota mikäkin olio ja milloin.

C++ haluaisi olioiden muodostavan puita, mutta todellisessa maailmassa suhteet ovat verkkoja.

Ratkaisu: boost::shared_ptr<T> muokkaa

Java, C# ja muut modernit sovellusohjelmointikielet ovat helppoja käyttää, koska taustalla toimiva roskienkerääjä hoitaa muistin vapauttamisen automaattisesti. Olioiden suhteet voivat olla hyvinkin väljiä. C++:ssa ei ole vakiona moista ylellisyyttä, koska se vie hieman tehoa. Sellainen on kuitenkin saatavilla ja suunnitteilla jopa tulevaan standardiin, joskin sen käyttö on vapaaehtoista ja siksi hankalampaa.

Oletetaan kuitenkin, että roskienkerääjää ei käytetä – siksihän C++ yleensä valitaan työkaluksi. Seuraavaksi paras vaihtoehto on viitteiden laskeminen. Kun viite luodaan, laskuriin lisätään yksi. Kun viite poistetaan, laskurista vähennetään yksi ja jos laskuri putosi nollaan, olio tuhotaan.

Boostin tärkein älykäs osoitin on shared_ptr, joka laskee viitteitä. Se alustetaan kuten auto_ptr. Sen avulla voidaan toteuttaa olioiden välisiä suhteita, jotka eivät ole hierarkisia. Esimerkiksi rasterikuvat vievät niin paljon muistia, että samaa kuvaa täytyy käyttää uudestaan eikä kopioida:

#include <boost/shared_ptr.hpp>
using boost::shared_ptr;

class Image {
    ...
};

class ImageBox {
private:
    shared_ptr<Image> im;
    ...
public:
    ImageBox(shared_ptr<Image> image) {
        im = image;
    }
    ...
};
 
    ...
    shared_ptr<Image> image(new Image("photo.jpg")); // viitelaskuri: 1
    imagebox1 = new ImageBox(image); // laskuri: 2 (väliaikaisesti 3)
    imagebox2 = new ImageBox(image); // laskuri: 3 (4)
} // laskuri: 2

// image tuhotaan heti, kun molemmat imageboxit ovat tuhottu
// (elleivät imageboxit kopioi shared_ptr-olioita eteenpäin)

Resurssien jaettu käyttö onkin yksi toimivimmista shared_ptr-luokan käyttötarkoituksista.

Jatko-ongelma: silmukkaviitteet muokkaa

Toisin kuin roskienkeräys, viitteiden laskeminen ei tuhoa olioita, joiden viitteet muodostavat silmukan. Yksinkertaisimmillaan kaksi toisiinsa viittaavaa oliota pitävät viitelaskurin nollaa suurempana. Luokkakaavion piirtämisessä kannattaakin olla tarkkana.

Joskus mallinnettavat käsitteet muodostavat silmukat, eikä sille voi mitään. Tähänkin ongelmaan tarvitaan ratkaisu.

Ratkaisu: heikot viitteet muokkaa

Silmukan muodostavat viitteet ratkaistaan tekemällä toisesta viitteestä heikko – sellainen, joka ei koske viitelaskuriin. On kuitenkin aina selvitettävä, aiheuttaako tämä ongelmia. Heikosti viitattu olio saattaa tuhoutua ilman ennakkovaroitusta.

Yksinkertaisin heikko viite on tavallinen osoitin (tai auto_ptr, scoped_ptr). Tämä on kelpo valinta, jos tiedetään, että viitattava olio ei voi tuhoutua alta pois.

Joskus on kuitenkin käsiteltävä tilanteita, jossa olioiden tuhoutumisjärjestystä ei voi tietää ennalta. Tällöin olisi tietenkin helpointa käyttää roskienkeräystä. Vaihtoehto on boost::weak_ptr, heikko osoitin, joka nollaantuu viitattavan olion tuhoutuessa.

Tietokonepelissä grafiikkamoottori ja fysiikkamoottori voivat pitää heikkoja viitteitä niitä kiinnostaviin peliolioihin. Kuitenkin peliolioiden täytyy voida tuhoutua pelisääntöjen seurauksena. Tällöin weak_ptr-osoittimet kuitenkin nollantuvat automaattisesti, ja ne voidaan ottaa pois kirjanpidosta.

Koska takeita elinajasta ei ole, niin weak_ptr on muutettava vahvaksi shared_ptr-osoittimeksi ennen olion käsittelyä. Tämä onnistuu jäsenfunktiolla lock().

Ohjeita viiteviidakon selvittämiseen muokkaa

Luokkakaavioon kannattaa ehdottomasti piirtää erinäköisin viivoin tiukan hierarkiset omistussuhteet, vahvat viitteet ja heikot viitteet; sitten tulee poistaa vahvat silmukat. Omistussuhteisiin käytetään automaattisia muuttujia (RAII), vahvoihin viitteisiin shared_ptr-osoitinta ja heikkoihin weak_ptr-osoitinta. Omistussuhde voi olla myös scoped_ptr tai auto_ptr, ja heikko viite omistajaan voi olla tavallinen osoitin.

Jos silmukoita on paljon ja suhteiden alistaminen heikoiksi on vaikeaa, on syytä harkita roskienkerääjää tai ajatella arkkitehtuuri uudelleen.

Ongelma: viitelaskuri on osoittimessa eikä oliossa muokkaa

Ratkaisu: boost::intrusive_ptr<T> muokkaa

Lähteet muokkaa