Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C++: templatea ja operaattoreita suunnittelemassa

vuokkosetae [17.09.2012 02:13:24]

#

Mitenhän tän näköistä tulisi suunnitella? Parikin A4:sta olen jo ajatellut ja aina olen kirjoittanut itseni jumiin keskelle paperia ilman ulospääsyä.

Ajattelin tunkea inttien unsignedien ja charria arvoja templatella luokkaan ja kirjoittaa niille operaattorit, jotka heittäisivät poikkeuksen ylivuodoista yms. Lisäksi mietein
bool muuta(T & tonne) joka vertaisi luokan sisällä olevaa tyyppin arvoa ja tonne tyypin maksimia tunkisi vasta sitten jos arvo sopisi tonne.

Tämähän on kohtuu helppo toteuttaa. Hillitön template luokka vain.

Miten se sitten kannattaa paketoida?
on ynseää kirjoittaa turvallinenluku<int> mun_intti. Helpompi olisi kaiketi kirjoittaa

int tmp = 42;
paketti_luokka foo(tmp);

paketti sitten ottaisi oikean soveltuvuuden käyttöön.

Jonkinlainen ratkaisu tuosta olisi pure virtual kantaluokan käyttö turvalliselleluvulle ja osoitinten kantaluokkaan ja dynaamisesti luoda noita sitten.

Paketilla olisi sitten yleisimmille muodostin kerrottu ja joku void paketti::muodostin(T arvo) tyyppinen, joka sitten muodostaisi noita vähemmän käytettyjä tyyppejä.

Käsittääkseni kuitenkin templateilla leikkiessä tulee tietää tyyppi käännösvaiheessa.

std::vector<paketti> vektori;
paketti tmp;
//täytetään se
tmp = vektori.at(rand()) + vektori.at(rand());
//ei nyt puututa rand()in käyttöön.

Tässä vaiheessa ei ole tiedossa mitä tulee tyypeiksi + operaation molemmille puolille.

Missä kohtaa ja kuinka tulisi ottaa huomioon nuo operaattorit? Ihan varmasti joku muukin on halunnut laskea omenoita ja päärynöitä, sekä tunkea ne samaan koriin. Sitten vain laskea koreilla tietenkin. Oon mennyt jossain metsään ja nyt en keksi kuinka sinne eksyin. Poispääsyssä kun lukee aina kirjoita koodia ihan tuhottomasti.

Ainakin Javalle googlella löytyy jotain :)

Metabolix [17.09.2012 16:50:58]

#

Ehkä auttaisi, jos kertoisit, mitä haluat saada aikaan, ja me vastaajat sitten kertoisimme sen järkevän tavan. Olen varma, että mahdollisia ratkaisuja on monta. Nykyisestä selityksestäsi en ymmärrä puoliakaan, ja kuulostaa, että olet keksinyt vain jonkin oudon (ja ilmeisesti viallisen) koodi-idean ilman järkevää käyttötarkoitusta.

Selvempää kysymystä miettiessäsi voit tutustua luokkiin boost::any ja boost::variant.

vuokkosetae kirjoitti:

std::vector<paketti> vektori;
paketti tmp = vektori.at(rand()) + vektori.at(rand());

Tässä vaiheessa ei ole tiedossa mitä tulee tyypeiksi + operaation molemmille puolille.

On tiedossa: molemmilla puolilla on paketti-tyyppinen olio. Muuta vaihtoehtoa ei ole, koska vektori sisältää suoraan olioita eikä osoittimia eikä siis voi koskaan sisältää muita kuin nimenomaan paketti-luokan olioita.

Tietenkin tilanne on toinen, jos vektoriin laitetaan osoittimia (ja kaavaan vastaavasti *-merkit). Silti molemmilla puolilla on edelleenkin paketti-tyyppinen olio. Tavallisen luokan tapauksessa kutsutaan silloin kyseisen luokan +-operaattoria tai ulkoista operaattoria. Jos taas +-operaattori on virtuaalinen, voidaan kutsua vasemmanpuoleisen olion todellisen luokan +-operaattoria. Näinhän virtuaalisten funktioiden kutsut toimivat. Parametrina on joka tapauksessa paketti-olio, eli mitään ihmeellistä arvailua ei tapahdu (eikä voi järkevästi tapahtuakaan, ongelmia olisi monta).

Tästä tilanteesta ei ole oikotietä onneen, vaan jos haluat käsitellä erilaisia paketteja eri funktioilla, joudut itse koodissa selvittämään tyypin (esim. typeid-operaattorilla, boost-kirjaston keinoilla tai erikseen tyyppiä ilmaisevalla jäsenellä) ja kutsumaan sopivaa jatkofunktiota.

eq [20.09.2012 21:59:37]

#

vuokkosetae kirjoitti:

Missä kohtaa ja kuinka tulisi ottaa huomioon nuo operaattorit? Ihan varmasti joku muukin on halunnut laskea omenoita ja päärynöitä, sekä tunkea ne samaan koriin. Sitten vain laskea koreilla tietenkin. Oon mennyt jossain metsään ja nyt en keksi kuinka sinne eksyin. Poispääsyssä kun lukee aina kirjoita koodia ihan tuhottomasti.

Kun omenoita ja päärynöitä laittaa samaan koriin, koreja tutkiessa pitää jokainen hedelmä tunnistaa uudelleen. C++ ei sisällä toiminnallisuutta koodipolkujen lisäämiseen ajonaikaisesti, joten jo käännösvaiheessa on oltava selvillä onko korissa vain omenoita ja päärynöitä, vai myös muita hedelmiä (ja mitä kullekin hedelmälle tulisi tehdä).

Erityyppisiä olioita, täysin toisiinsa liittymättömiäkin, voi kyllä laittaa samaan koriin; tai tietorakenteeseen. Tällöin kadotetaan kuitenkin tieto kunkin olion tarkasta tyypistä, ja se pitää jonnekin tallettaa erikseen. Vaikeus nimenomaisesti tulee vasta siinä vaiheessa, kun olion haluaa saada takaisin tarkkaan tyyppiinsä.

Esimerkissäsi et näytä laskevan koreilla, vaan korien sisällöillä (olettaen, että paketti käärii jonkun hedelmän, kuten yllä annat olettaa). Tämä ei ole mahdotonta, mutta varsinkin nyt sinun pitää tarkkaan määritellä mitä operaatio tarkoittaa. Paljonko on päärynä + omena?

vuokkosetae kirjoitti:

Miten se sitten kannattaa paketoida?
on ynseää kirjoittaa turvallinenluku<int> mun_intti. Helpompi olisi kaiketi kirjoittaa

int tmp = 42;
paketti_luokka foo(tmp);

En voi väittää olevani samaa mieltä. Paitsi että helppoudessa sinänsä ei näy olevan mitään eroa, siinä missä tyyppi turvallinenluku<int> tietää sisältönsä tyypin, paketti_luokka ei sitä voi samalla tavalla (staattisesti) tallettaa mitenkään.

Jos se, mitä oikeasti kammoksut on int-tyypin näennäisen redundantti ja tilaa vievä toistaminen, mitä oikeasti haluat on vapaa apufunktio, kuten tällainen:

template<typename T>
CheckedNumber<T> make_checked(T value)
{
  return CheckedNumber<T>(value);
}

vuokkosetae kirjoitti:

std::vector<paketti> vektori;

Milloin ihmeessä sinun tarvitsisi laittaa eri tyyppisiä numeroita (turvallisia tahi ei) samaan tietorakenteeseen?

Metabolix kirjoitti:

Tietenkin tilanne on toinen, jos vektoriin laitetaan osoittimia (ja kaavaan vastaavasti *-merkit). Silti molemmilla puolilla on edelleenkin paketti-tyyppinen olio.

Molemmilla puolilla on tietenkin viittaus paketti-tyyppiseen olioon, sillä muutenhan ei olisi käännöksen aikanakaan epäselvyyttä siitä, mitä funktiota kutsutaan (oli se virtuaalinen tai ei).

--

Virtuaalinen +-operaattori on jossain määrin ongelmallinen, sillä sen pitäisi kaanonisesti palauttaa olio (ei viittausta sellaiseen), jolloin perityt luokat eivät voisi muuttaa palautettavan arvon tyyppiä. Myöskään abstrakti luokka ei tällaista operaattoria voisi määritellä.

Koska minulla oli luppoaikaa (ja ehkä vähän tylsääkin...), päätin kirjoitella nopeasti jotain, joka vastasi aloitusviestin kuvausta. Alla olevassa on tehty selvä tradeoff mahdollisten numeroluokkien määrän ja eri numeroluokkien yhdessäkäytettävyyden välillä. Virtuaaliset funktiot tekevät ajonaikaisen oikean funktion (ja operaattorin) löytämisen helpoksi: vaikeus tulee eteen, kun haluat päästä käsiksi itse hukattuun tyyppiin:

#include <cstdint>
#include <iostream>
#include <limits>
#include <memory>
#include <type_traits>
#include <typeinfo>

class CheckedBase {
public:
   CheckedBase(const std::type_info& type_info): type_info(type_info) {}
   virtual void operator+=(const CheckedBase&) = 0;
   virtual std::unique_ptr<CheckedBase> clone() const = 0;
   virtual bool is_signed() const = 0;
   virtual std::intmax_t as_signed() const = 0;
   virtual std::uintmax_t as_unsigned() const = 0;
   const std::type_info& type() const { return type_info; }
private:
   const std::type_info& type_info;
};

template<typename T>
class CheckedNumber: public CheckedBase {
public:
   CheckedNumber(T value): CheckedBase(typeid(T)), value(value) {}
   void operator+=(const CheckedBase& rhs)
   {
      // check for overflows
      if (rhs.is_signed())
         value += rhs.as_signed();
      else
         value += rhs.as_unsigned();
      // jos ei tarvitse sekamehua, voi käyttää esim. seuraavaa (tällöin on mahdollista
      // käyttää myös tyyppejä, jotka eivät mahdu *intmax_t-muuttujiin):
      // if (type() == rhs.type())
      //   value += reinterpret_cast<const CheckedNumber<T>&>(rhs).value;
   }
   std::unique_ptr<CheckedBase> clone() const
   {
      return std::unique_ptr<CheckedBase>(new CheckedNumber<T>(*this));
   }
   bool            is_signed()   const { return std::is_signed<T>::value; }
   std::intmax_t   as_signed()   const { return value; }
   std::uintmax_t  as_unsigned() const { return value; }
private:
   T value;
};

class Wrapper {
public:
   template<typename T>
   Wrapper(T value): number(new CheckedNumber<T>(value)) {}
   Wrapper(const Wrapper& rhs): number(rhs.number->clone()) {}
   Wrapper operator+(const Wrapper& rhs)
   {
      Wrapper tmp(*this);
      *tmp.number += *rhs.number;
      return tmp;
   }
   const CheckedBase& get_number() const
   {
      return *number;
   }
private:
   std::unique_ptr<CheckedBase> number;
};

int main()
{
   Wrapper p(42);
   Wrapper u((short)25);

   Wrapper n = p + u;

   std::cout << n.get_number().as_signed() << "\n";

   // n tosiaan säilöö int-tyyppisen tarkistetun luvun
   std::cout << (n.get_number().type() == typeid(int)) << "\n";

   // yleisessä tapauksessa ei kuitenkaan ole triviaalia tapaa saada Wrapperista
   // SafeNumber<T>& :tä, sillä staattinen (käännöksen aikainen) tyyppitieto on kadotettu.
}

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta