Vaikeustaso: henkilölle, joka osaa määritellä C++:ssa yksinkertaisen luokan.
Hyödyllisyys: selkeyttää ja lyhentää melkein mitä tahansa C++-koodia, jossa käsitellään merkkijonoja.
Lyhyesti
Kaksi kätevää apuvälinettä C++-arvojen muuntamiseksi tekstimuotoon. Ensiksi näytetään, miten itse määritellyt luokat saadaan tulostumaan yhtä vaivattomasti kuin int
ja float
. Tämän jälkeen annetaan makro, joka lyhentää merkkijonoja rakentavaa koodia.
Omien tietotyyppien tulostus
Jokainen C++:aa kirjoittanut tietää, miten helppoa siinä on erityyppisten muuttujien tulostus. std::cout << x
toimii,
x
:n ollessa esimerkiksi std::string
, int
, double
tai const char*
. Entäpä, jos x
onkin jokin itse määrittelemäsi luokka?
C++ mahdollistaa merkkijonotulostuksen laajentamisen omiin tyyppeihin näennäisesti vaikean näköisellä mutta oikeasti melko vaivattomalla tavalla. Temppu tehdään kuormittamalla globaalia <<
-operaattoria. Katsotaanpa esimerkiksi yksinkertaista pistettä esittävää luokkaa. Yksityiskohtainen selitys tulee koodin jälkeen.
#include <ostream> // (std::ostream on kaikkien tulostusvirtojen yliluokka) struct Piste { Piste(int x, int y) : x(x), y(y) {} const int x, y; // Piste-olioiden tulostus friend std::ostream& operator<< (std::ostream& s, const Piste& p) { s << "(" << p.x << "," << p.y << ")"; return s; } }; // ... #include <iostream> int main() { Piste p(4, 2); std::cout << "Hei numero " << 13 << ", olet pisteessä: " << p << std::endl; return 0; }
Selitystä:
- Ensimmäinen parametri esittää operaattorin vasenta puolta, eli tässä tapauksessa tulostuksen vastaanottavaa virtaa. Otamme siihen viitteen, jotta siitä ei yritettäsi tehdä kopiota.
- Toinen parametri on viite omaan luokkaamme. Ilmaisemme const
-määreellä, että emme aio tulostaessamme muuttaa olion sisältöä.
- Palautamme viitteen saamaamme tulostusvirtaan. Tämä mahdollistaa <<
-operaattorien ketjutuksen. Esim. std::cout << a << b;
, joka on sama kuin (std::cout << a) << b;
toimii, koska ensimmäinen <<
-operaatio palauttaa viitteen std::cout:
iin, jota toinen <<
sitten käyttää.
- friend
-määre kertoo, että kyse on globaalista operator<<
-funktiosta, ei siis luokan jäsenfunktiosta. friend
antaa lisäksi funktiolle pääsyn luokan Piste
private
-jäseniin, vaikka tätä ei esimerkissämme tarvittukaan.
- Operaattorimääritys voidaan vaihtoehtoisesti kirjoittaa friend
-määrettä lukuunottamatta täsmälleen samassa muodossa luokan ulkopuolelle. Näin voidaan määrittää tulostusoperaattori myös muiden kirjoittamille tyypeille, kuiten vaikka SDL-kirjaston SDL_Rect
:lle. Ilman friend
-määrettä operaattori ei kuitenkaan voi lukea olion mahdollisia private
-jäseniä.
Tulostus merkkijonomuuttujaan yhdellä rivillä
Tavallisin tapa muuntaa kokonais- tai liukulukuja (tai äskeisen perusteella oikeastaan mitä vain) merkkijonoiksi on käyttää std::stringstream
-luokkaa <<
operaattorin kohteena näin:
#include <string> #include <sstream> std::string summaTekstina(int x, int y) { std::stringstream ss; ss << x << " + " << y << " = " << (x+y); return ss.str(); }
Tähän tarvitaan kuitenkin kolme kokonaista lausetta, joiden kirjoittaminen on monessa tilanteessa kiusallista. Esimerkiksi poikkeukselle olisi kätevää antaa virheviesti vaikkapa näin:
if (mentiinMetsaan) throw IsoPahaPoikkeus(MKSTR("Eksyttiin metsään, jossa on " << x << " puuta"));
MKSTR
:n toteutus on melko mystinen. Kaikkiin yksityiskohtiin en ole itsekään vielä saanut tyydyttävää selitystä.
#include <sstream> #define MKSTR(args) (static_cast<std::ostringstream &>(std::ostringstream() << std::flush << args).str()) // Käyttöesimerkki: std::string s = MKSTR(123 << " on paljon" << std::endl << 456 << " on enemmän"); // (testattu uudehkoilla GCC- ja MSVC-kääntäjillä)
Selostusta:
- std::ostringstream()
luo tilapäisen ostringstream
-olion (ostringstream
on stringstream
ilman meille tarpeetonta lukuvirtaa).
- Virtaan työnnetään turha std::flush
-merkki, koska ilman sitä ainakin GCC tulkitsisi ensimmäisenä olevan const char*
-argumentin void*
-osoittimena ja tulostaisikin tekstin osoitteen heksadesimaalimuodossa. En osaa edes arvata, miksi asia on näin.
- Seuraavaksi virtaan työnnetään makron argumentit, jotka siis voivat sisältää lisää <<
-operaatioita.
- <<
-operaattorit palauttavat tilapäiseen merkkijonovirtaamme std::ostream&
-tyyppisen viitteen. Se on muunnettava takaisin std::ostringstream
-viitteeksi, jotta lopuksi voidaan kutsua sen str()
-metodia.
No selvisipä vihdoin, miksi se ensimmäinen teksti tuli void*:na ulos. :) Kelpo vinkki muutenkin.
Vinkin alun operaattorijuttu käsitellään jo eräässä vanhassa vinkissäni.
Heipparalla pitkästä aikaa. Asiallista tarinaa operaattorien kuormittamisesta. Mutta:
lainaus:
Virtaan työnnetään turha std::flush-merkki, koska ilman sitä ainakin GCC tulkitsisi ensimmäisenä olevan const char*-argumentin void*-osoittimena ja tulostaisikin tekstin osoitteen heksadesimaalimuodossa. En osaa edes arvata, miksi asia on näin.
No siksi kun tuossa makrossa luodaan tilapäinen ostringstream-olio ja jolloin mitään basic_ostream-luokan ulkopuolella määriteltyä <<-operaattoria ei voi käyttää, koska ne vaativat ei-const parametrin. Luokan sisällä määriteltyjä juttuja voidaan kyllä käyttää ja niistä void const * -parametrin ottava <<-operaattori on pointterityypin muunnoksen jälkeen lähinnä paras vaihtoehto.
Makroja ei juurikaan kannata käyttää.
Enkä niin hirveästi perusta jonkun kolmen koodirivin säästämisestäkään, jos koodi muuten muuttuu jollakin tapaa epäselvemmäksi. Jos nyt on ihan pakko jotain säästövinkkejä soveltaa, niin ehkä sitten vaikka näin:
#include <sstream> #include <ostream> #include <string> #include <iostream> class tmpstr { std::ostringstream o; public: template<typename T> tmpstr &operator<<(T const &a) { o << a; return *this; } operator std::string() const { return o.str(); } }; int main() { std::string s = tmpstr() << 123 << " on paljon\n" << 456 << " on enemmän"; std::cout << "[" << s << "]\n"; }
Omat ongelmansa on tässäkin, ei tuo tmpstr:kään ihan kaikkeen sovi.
Jos selkeyttä hakee, niin kyllä oman pienen paikallisen funktion käyttö voi sitten kuitenkin olla se paras vaihtoehto. Hautaa sitten ne hirmuiset kolme riviä sitten sinne naapurustoon:
if (mentiinMetsaan) HeitaPoikkeusMetsaanmenemisesta(x);
Itse olen käyttänyt vastaavaan tarkoitukseen tällaista funktiota:
#include <sstream> template <typename T> std::string to_string(T value) { std::ostringstream oss; oss << value; return oss.str(); }
Voi sitten käyttää Javan tapaan merkkijonojen plusoperaattoria.
if(a!=5) throw OmaPoikkeus("Muutujan a arvo on " + to_string(a) + " eikä 5.");
Mielestäni +
on paljon selkeämpi, kuin tuo <<
-operaattori yhdistämässä (lyhyitä) merkkijonoja, varsinkin silloin, kun yhdistetty merkkijono annetaan funktion parametriksi. Itse jätän usein const &
-määreet suosiolla pois tämän kaltaisten funktioiden parametreistä. Tämä kohta ei varmasti vaikuta ohjelman tehokkuuten millään tavalla, joten ei ole väliä, kopioidaanko tässä pari merkkijonoa vai ei. const
-viittaukset usein tuottavat ongelmia tuollaisten väliaikaismuuttujien kanssa.
Tässä on ongelmana se, että copy-konstruktorilla (varsinkin kääntäjän luomalla) varustettujen vääränlaisten olioiden syöttäminen tuolle funktiolle johtaa todennäköisesti katastrofiin, jos std::ostream &operator<<
on määritelty.
Itse en kuitenkaan ole kokenut tuota kaikkien mahdollisten objektien tulostusmahdollisuutta kovin tarpeelliseksi. Mitä tarkoittaa "std::cout << Avaruusalus(x);
" ? Mieluummin käyttää havainnollisempia funktioita:
throw OmaPoikkeus("Avaruusalus " + alus.nimi() + " on ruudun ulkopuolella, koordinaatit: " + to_string(alus.paikka()));
Aihe on jo aika vanha, joten et voi enää vastata siihen.