1Miten kustomoidut purkajat (custom deleters) toimivat älykkäissä osoittimissa, ja milloin niistä on hyötyä C/C++ -yhteentoimivuudessa?
Kustomoitu purkaja on kutsuttava siivouslogiikka, jota älykäs osoitin käyttää oletusarvoisen `delete`-operaation sijaan vapauttaessaan omistamansa resurssin. Siitä on hyötyä C/C++ -yhteentoimivuudessa silloin, kun resurssi on vapautettava tietyllä funktiolla, kuten `fclose`, `close`, `curl_easy_cleanup`, `SSL_free`, `free` tai jollakin kirjastokohtaisella tuhoamisfunktiolla. `unique_ptr`:n tapauksessa purkajan tyyppi on osa `unique_ptr`:n tyyppiä ja voi siten vaikuttaa sen muistinkulutukseen; tilattomat purkajat voidaan optimoida pois, kun taas funktio-osoittimet tai tilalliset purkajat vaativat lisätilaa. `shared_ptr`:n kohdalla purkaja tallennetaan ohjauslohkoon (control block) ja se suoritetaan, kun viimeinen vahva omistaja vapauttaa olion. Kustomoitujen purkajien avulla C-resurssit voivat osallistua turvallisesti RAII (Resource Acquisition Is Initialization) -malliin.
2Selitä `weak_ptr`- ja `shared_ptr`-älyosoittimien ohjauslohkon (control block) toimintamekanismit, mukaan lukien syklit, `enable_shared_from_this` ja viitelaskureiden kustannukset.
`shared_ptr` hallitsee jaettua omistajuutta ohjauslohkon (control block) kautta, joka sisältää vahvat ja heikot viitelaskurit sekä siivoustietoja, kuten purkajan (deleter) ja varaajan (allocator). `shared_ptr`-olioiden kopiointi tai tuhoaminen kasvattaa tai pienentää vahvaa viitelaskuria. Tämä tapahtuu tyypillisesti atomisilla operaatioilla, joten erillisiä `shared_ptr`-olioita voidaan käsitellä turvallisesti useissa säikeissä, mutta jokaisella viitelaskurin päivityksellä on suorituskykykustannus. Kun vahva viitelaskuri nollautuu, hallittu olio tuhotaan; ohjauslohko säilyy, kunnes myös heikot viittaukset on poistettu. `weak_ptr` osoittaa samaan ohjauslohkoon pidentämättä olion elinkaarta; `lock()` palauttaa `shared_ptr`-arvon, jos olio on edelleen elossa, ja muussa tapauksessa tyhjän `shared_ptr`-arvon. Pelkistä `shared_ptr`-olioista muodostuvat syklit aiheuttavat muistivuodon, koska vahvat viitelaskurit eivät koskaan saavuta nollaa. Siksi `weak_ptr`-osoittimia käytetään paluuviittauksiin tai tarkkailijalinkkeihin. `enable_shared_from_this` antaa olion, jonka `shared_ptr` jo omistaa, luoda uuden `shared_ptr`-osoittimen itseensä olemassa olevaa ohjauslohkoa käyttäen, mikä välttää vaaralliset erilliset ohjauslohkot. Viitelaskurin kustannuksiin kuuluvat atomiset lisäykset ja vähennykset, välimuistikilpailu (cache contention), ohjauslohkon muistinvaraus ja ylimääräinen kuorma suorituskyvyn kannalta kriittisissä koodin osissa.
3Selitä C++:n arvokategoriat (value categories) ja täydellinen välittäminen (perfect forwarding) sekä se, miksi ne ovat tärkeitä tehokkaissa ja geneerisissä API-rajapinnoissa.
C++:n arvokategoriat kuvaavat lausekkeita: lvalue-arvoilla on identiteetti ja niihin voidaan viitata lausekkeen jälkeen; prvalue-arvot ovat puhtaita rvalue-arvoja, kuten monet väliaikaiset tai lasketut arvot; xvalue-arvot ovat elinkaarensa päässä olevia olioita, joiden resurssit voidaan käyttää uudelleen. Täydellinen välittäminen on mallipohjainen tekniikka, jossa käytetään välittävää viittausta (forwarding reference), yleensä `T&&`, jossa `T` on päätelty tyyppi, ja argumentti välitetään `std::forward<T>(arg)` -funktiolla. Näin kutsujan arvokategoria säilyy: lvalue-arvot pysyvät lvalue-arvoina ja rvalue-arvot rvalue-arvoina. Viittausten yhdistämissäännöt (reference collapsing) tekevät tämän mahdolliseksi. Tämä on tärkeää taustajärjestelmien geneerisissä API-rajapinnoissa, koska kääreluokat, tehtaat, sanomanvälittäjät (dispatchers) ja `emplace`-tyyppiset funktiot voivat välttää tarpeettomat kopiot ja säilyttää ylikuormituksen valinnan (overload selection) sekä siirtosemantiikan toiminnan.
4Vertaile `auto`-, `decltype`- ja `decltype(auto)`-avainsanoja sekä mallien argumenttien päättelyä (template argument deduction) tyypillisessä taustajärjestelmäkoodissa.
`auto` käyttää mallien kaltaista päättelyä muuttujille: pelkkä `auto` jättää yleensä pois viittaukset ja ylimmän tason `const`-määreet, ellei esittelyssä erikseen pyydetä niitä (esim. `auto&`, `const auto&` tai `auto&&`). `decltype(expr)` tutkii esitellyn tyypin tai lausekkeen tyypin tarkemmin: sulkeeton id-lauseke antaa esitellyn tyypin, kun taas muut `lvalue`-lausekkeet tuottavat tyypin `T&`, `xvalue`-lausekkeet tyypin `T&&` ja `prvalue`-lausekkeet tyypin `T`. `decltype(auto)` päättelee tyypin käyttäen `decltype`-sääntöjä, ja sitä käytetään usein palautustyypeissä silloin, kun viittaukset on säilytettävä. Mallien argumenttien päättely on samankaltainen kuin `auto`, mutta se riippuu parametrin muodosta (kuten `T`, `T&`, `const T&` tai `T&&`) ja sillä on omat sääntönsä. Aaltosulkeilla tehtävät alustukset ovat yleinen ero: `auto x = {1,2}` päättelee tyypiksi `std::initializer_list<int>`, kun taas tavallinen malliparametri ei yleensä pysty päättelemään tyyppiä `T` pelkästä aaltosulkualustuksesta, ellei parametri odota `initializer_list`-tyyppiä tai muuta sopivaa tyyppiä.
5Vertaile `std::variant`-tyyppiä ja perintään perustuvaa polymorfismia heterogeenisten viestien tai tapahtumien mallintamisessa.
`std::variant` on arvotyyppi, joka sisältää tarkalleen yhden vaihtoehdon kiinteästä, suljetusta tyyppijoukosta. Sitä käsitellään yleensä `std::visit`-funktiolla tai eksplisiittisillä tyyppikyselyillä. Se on hyödyllinen protokollaviestien tai tapahtumien kohdalla silloin, kun viestityyppien joukko on tiedossa ja halutaan tyyppiturvallista käsittelyä ilman virtuaalifunktiokutsuja (virtual dispatch) ja usein ilman oliokohtaista keon muistinvarausta. Perintään perustuva polymorfismi käyttää perusluokkaa ja virtuaalifunktioita ohjaamaan kutsut yhteisen rajapinnan kautta. Se on parempi vaihtoehto silloin, kun johdettujen viestityyppien joukko on avoin, itsenäisesti laajennettavissa, liitännäistyyppinen tai piilotettu vakaan rajapinnan taakse. Variantti suosii suljettuja summatyyppejä, muistin paikallisuutta ja käännösaikaista tarkistusta; perintä suosii laajennettavuutta, ajonaikaista polymorfismia ja rajapintapohjaista suunnittelua.
6Mitä on määrittelemätön toiminta (undefined behavior) C++-kielessä, ja miten se voi ilmetä taustajärjestelmien (backend) tuotantoympäristön häiriötilanteissa?
Määrittelemätön toiminta on toimintaa, jolle C++-standardi ei aseta mitään vaatimuksia virheellisen operaation tapahduttua. Ohjelma saattaa vaikuttaa toimivan normaalisti, kaatua, korruptoida dataa, altistaa tietoturva-aukoille tai tulla optimoiduksi tuottamaan yllättävää toimintaa. Kääntäjät olettavat, ettei määrittelemätöntä toimintaa (UB) tapahdu, ja tekevät optimointeja tähän oletukseen perustuen, joten ongelmat saattavat ilmetä vasta julkaisuversioissa (release build) tai tuotantoliikenteen alla. Taustajärjestelmien häiriöt voivat johtua roikkuvista osoittimista tai viittauksista (dangling pointers/references), vapautetun muistin käytöstä (use-after-free), olioiden elinkaarisääntöjen rikkomuksista, rajojen ulkopuolisesta muistipääsystä, etumerkillisten kokonaislukujen ylivuodosta, datakilpailuista (data races), virheellisistä tyyppimuunnoksista, alustamattoman muistin lukemisesta, kahdesti vapauttamisesta (double free) tai strict aliasing -sääntöjen rikkomuksista. Torjuntakeinoihin kuuluvat RAII (Resource Acquisition Is Initialization) ja selkeä omistajuuden sekä elinkaaren suunnittelu, turvallisemmat abstraktiot ja rajatarkistukset, testaus ja sumeustestaus (fuzzing), koodikatselmoinnit, staattinen analyysi sekä sanitizer-työkalut, kuten ASan, UBSan ja TSan.
7Miten määrittelemätön (undefined), täsmentämätön (unspecified) ja toteutuskohtainen (implementation-defined) toiminta eroavat toisistaan? Anna esimerkkejä taustajärjestelmäkehityksen näkökulmasta.
Määrittelemätön toiminta tarkoittaa, että C++-standardi ei aseta mitään vaatimuksia. Seurauksena voi olla ohjelman kaatuminen, datan korruptoituminen, tietoturvabugeja tai optimoijasta riippuvaa virheellistä konekoodia. Esimerkkejä ovat taulukon rajojen ylittävä muistihaku, vapautetun muistin käyttö (use-after-free), etumerkillisen kokonaisluvun ylivuoto ja kilpatilanteet (data race). Täsmentämätön toiminta tarkoittaa, että standardi sallii useamman kuin yhden lopputuloksen, eikä toteutuksen tarvitse dokumentoida, mikä niistä valitaan. Yleinen esimerkki on funktioiden argumenttien evaluointijärjestys, joten koodin ei pitäisi riippua sivuvaikutusten suoritusjärjestyksestä. Toteutuskohtainen toiminta tarkoittaa, että toteutuksen on valittava ja dokumentoitava toimintatapa. Esimerkkejä ovat tavallisen `char`-tyypin etumerkillisyys, joidenkin perusteistyyppien koot ja rajat standardin puitteissa sekä etumerkillisen bittisiirron käyttäytyminen oikealle. Taustajärjestelmäkoodissa UB (Undefined Behavior) on oikeellisuus- ja tietoturvariski, kun taas täsmentämätön ja toteutuskohtainen toiminta ovat siirrettävyysriskejä, joita tulisi välttää tai jotka pitäisi eristää protokolla-, tallennus- ja alustariippumattomaan logiikkaan.
8Selitä poikkeusturvallisuuden tasot ja kuinka `noexcept` vaikuttaa siirto-operaatioihin, säiliöihin ja koodin generointiin.
Poikkeusturvallisuuden tasot kuvaavat sitä, mikä pysyy totena, jos operaatio heittää poikkeuksen. Perustakuu (basic guarantee) tarkoittaa, että invariantit säilyvät ja resursseja ei vuoda, vaikka tilamuutoksia on saattanut tapahtua. Vahva takuu (strong guarantee) tarkoittaa vahvista-tai-peruuta -semantiikkaa (commit-or-rollback): operaation epäonnistuessa havaittavissa oleva tila pysyy täysin muuttumattomana. Ei-heittävä takuu (nothrow guarantee) tarkoittaa, että operaatio ei heitä poikkeuksia. Yleisiä tekniikoita poikkeusturvallisuuden saavuttamiseksi ovat RAII (Resource Acquisition Is Initialization), työn suorittaminen väliaikaisilla olioilla, kopioi-ja-vaihda (copy-and-swap) -idiomi ja tilamuutosten järjestäminen niin, että vahvistaminen (commit) tapahtuu vasta, kun poikkeuksia mahdollisesti heittävä työ on onnistunut. `noexcept` on sopimus: jos `noexcept`-funktio heittää poikkeuksen, kutsutaan välittömästi `std::terminate`-funktiota. Se vaikuttaa myös geneeriseen koodiin ja säiliöihin: esimerkiksi muistin uudelleenallokoinnin aikana `std::vector` voi siirtää alkioita, kun siirtokonstruktori on `noexcept`; muussa tapauksessa se saattaa kopioida ne, usein `std::move_if_noexcept`-funktion avulla, säilyttääkseen poikkeusturvallisuuden takuut. `noexcept` voi myös auttaa koodin generoinnissa ja optimoinnissa vähentämällä tarvittavia poikkeusten etenemispolkuja, mutta vain silloin, kun koodin antama lupaus pitää paikkansa.
9Selitä `std::expected` tai vastaavat tulostyypit vaihtoehtoina poikkeuksille virhealttiissa operaatioissa.
`std::expected<T, E>` edustaa joko onnistunutta arvoa `T` tai eksplisiittistä virhettä `E`, mikä tekee epäonnistumisesta osan funktion paluuarvotyyppiä sen sijaan, että luotettaisiin poikkeusten välittymiseen. Se on hyödyllinen virhealttiissa operaatioissa, joissa virheet ovat odotettavissa ja kutsujien tulisi käsitellä ne paikallisesti. Tällaisia ovat esimerkiksi jäsennys, validointi, verkkokutsut, tietokantakyselyt ja uudelleenyritettävät taustajärjestelmän operaatiot. Virhetyypin tulisi kantaa rakenteellista tietoa, kuten virhekoodi, kategoria, uudelleenyritettävyys, viesti tai HTTP/RPC-kartoitus. `expected`- ja muut tulostyypit mahdollistavat virheiden tarkistamisen ja edelleen välittämisen komposition avulla, ja C++23-tyylisissä API-määrittelyissä (Application Programming Interface) voidaan käyttää monadisia operaatioita, kuten `and_then`, `transform` ja `or_else`, vaiheiden ketjuttamiseen ilman syvälle sisäkkäin meneviä ehtolauseita. Verrattuna poikkeuksiin ne tekevät ohjausvuosta ja API-sopimuksista eksplisiittisiä ja toimivat hyvin poikkeuksettomissa ympäristöissä tai ABI-rajojen (Application Binary Interface) yli; poikkeukset voivat silti olla sopivia harvinaisiin, ei-paikallisiin tai todella poikkeuksellisiin virhetilanteisiin projektin käytännöistä riippuen.
10Kuvaile olioiden elinkaarisäännöt automaattisille, dynaamisille, tilapäisille ja asynkronisesti viitatuille olioille.
Automaattiset oliot ovat olemassa luomisestaan näkyvyysalueensa loppuun asti; niihin osoittavista osoittimista tai viitteistä tulee roikkuvia näkyvyysalueesta poistumisen jälkeen. Dynaamiset oliot ovat olemassa varaamisestaan ja luomisestaan alkaen, kunnes ne nimenomaisesti tuhotaan tai omistava olio tuhoaa ne; raakaosoittimet ja viitteet eivät pidennä elinkaarta. Tilapäiset oliot elävät yleensä koko lausekkeen loppuun asti. Niihin liittyy erityisiä elinkaaren pidentämissääntöjä, kun ne sidotaan viitteisiin, mutta tämä ei päde jokaiseen käyttötapaukseen. Asynkroniset toiminnot, kuten takaisinkutsut, säikeet, ajastimet tai korutiinit, voivat suorittua sen jälkeen, kun viitattu olio on tuhottu. Tämän vuoksi kaapatut muuttujat (captures) ja tallennetut viitteet vaativat nimenomaista elinkaaren hallintaa roikkuvien viitteiden ja use-after-free-virheiden välttämiseksi.
11Kuvaile staattisten muuttujien alustusjärjestyksen ongelmia ja miten `constinit`, magic statics -rakenteet ja riippuvuuksien injektointi lieventävät niitä.
Staattisten muuttujien alustusjärjestyksen ongelmia syntyy, koska dynaamisesti alustettujen nimiavaruustason (namespace-scope) tai staattisten olioiden suhteellinen alustusjärjestys on määrittelemätön eri käännösyksiköissä (translation units). Yksi globaali muuttuja saattaa käyttää toista ennen kuin se on konstruoitu; myös purkujärjestys (destruction order) voi aiheuttaa vastaavia ongelmia ohjelman sammutuksen yhteydessä. `constinit` pakottaa staattisen tai säiekohtaisen muuttujan käyttämään staattista tai vakioalustusta, tai ohjelma on virheellinen (ill-formed); tämä välttää kyseisen muuttujan riippuvuuden dynaamisesta alustusjärjestyksestä. Magic statics, eli funktiokohtaiset staattiset muuttujat, alustetaan ensimmäisellä käyttökerralla ja ne ovat olleet säieturvallisia C++11-standardista lähtien. Riippuvuuksien injektointi välttää piilotetut globaalit riippuvuudet konstruoimalla oliot hallitussa järjestyksessä ja välittämällä riippuvuudet eksplisiittisesti.
12Mitä ovat kopion poisto, NRVO (Named Return Value Optimization) ja RVO (Return Value Optimization), ja milloin niihin voi luottaa?
Kopion poisto tarkoittaa olion luomista suoraan sen lopulliseen sijaintiin sen sijaan, että luotaisiin erillinen väliaikainen olio, joka kopioidaan tai siirretään. RVO viittaa yleensä nimeämättömän väliaikaisen olion tai `prvalue`-arvon palauttamiseen, kuten `return T{};`. C++17:ssä ja uudemmissa versioissa monet näistä tapauksista ovat pakollisia, koska `prvalue` alustaa tulosolion suoraan. NRVO tarkoittaa nimetyn paikallisen olion palauttamista, kuten `return x;`. Kääntäjän on sallittua luoda tämä paikallinen olio suoraan kutsujan paluuarvon paikkaan, mutta sitä ei taata. Voit luottaa pakolliseen C++17:n `prvalue`-arvojen kopion poistoon tietyissä tapauksissa, mutta et voi luottaa siihen, että NRVO tapahtuu aina. Vältä `std::move`-kutsun käyttöä nimetyn paikallisen paluuarvon kohdalla, koska se voi estää NRVO:n.
13Miten vertailija-, yhtäsuuruus- ja tiivistefunktiovaatimukset vaikuttavat assosiatiivisten säiliöiden oikeellisuuteen?
Assosiatiiviset säiliöt luottavat vertailuun, yhtäsuuruuteen ja tiivistämiseen avainten identiteetin määrittämiseksi ja sisäisten invarianttiensa ylläpitämiseksi. Järjestetyt säiliöt, kuten `std::map`, vaativat vertailijalta tiukkaa heikkoa järjestystä (strict weak ordering); avaimia pidetään samanarvoisina, kun kumpikaan ei ole pienempi kuin toinen, ei välttämättä `operator==`-operaattorin perusteella. Järjestämättömät säiliöt vaativat, että yhtäsuuruus on ekvivalenssirelaatio, ja minkä tahansa kahden yhtäsuurena pidetyn avaimen on tuotettava sama tiivistearvo. Avaimia ei saa muuttaa tallennuksen aikana tavalla, joka muuttaa niiden järjestystä, yhtäsuuruutta tai tiivistettä, sillä se voi johtaa virheelliseen toimintaan avainten haussa, poistossa ja ainutlaatuisuuden varmistamisessa.
14Selitä heterogeeninen haku järjestetyissä ja järjestämättömissä assosiatiivisissa säiliöissä.
Heterogeeninen haku (heterogeneous lookup) mahdollistaa assosiatiivisen säiliön hakemisen käyttämällä eri tyyppiä kuin säiliön avaintyyppi, mikä välttää väliaikaisten avainten luomisen. Järjestetyissä säiliöissä (ordered containers) tämä vaatii läpinäkyvän vertailijan, esimerkiksi `std::less<>` tai mukautetun vertailijan, jossa on `is_transparent`-merkintä. Järjestämättömissä säiliöissä (unordered containers) se edellyttää sekä läpinäkyvää tiivistefunktoria (hash functor) että läpinäkyvää yhtäsuuruusfunktoria, jotka pystyvät käsittelemään avaintyyppiä ja hakutyyppiä yhdenmukaisesti. Yleinen taustajärjestelmäesimerkki on `std::string`-avaimilla varustettu säiliö, josta voidaan hakea käyttämällä `std::string_view`- tai `const char*` -tyyppejä ilman, että joudutaan varaamaan muistia väliaikaiselle `std::string`-oliolle.
15Selitä C++:n muistimalli: datakilvat (data races), happens-before-suhde ja synkronointitakuut.
C++:n muistimalli määrittelee, milloin eri säikeissä tapahtuvat operaatiot on järjestetty ja milloin kirjoitukset tulevat näkyviksi. Datakilpa (data race) syntyy, kun kaksi säiettä käsittelee samaa muistipaikkaa samanaikaisesti, vähintään toinen käsittelyistä on kirjoitus, ja näitä käsittelyitä ei ole järjestetty happens-before-suhteella tai muuten tehty turvallisiksi atomisilla operaatioilla. Datakilpa aiheuttaa määrittelemättömän toiminnan (undefined behavior). Happens-before on järjestyssuhde, joka tekee aiemmista sivuvaikutuksista näkyviä myöhemmille operaatioille. Synkronointioperaatiot luovat tämän järjestyksen: esimerkiksi mutexin lukituksen avaaminen synkronoituu saman mutexin myöhemmän onnistuneen lukituksen kanssa, ja sopivat atomiset release- ja acquire-operaatiot voivat synkronoida säikeiden välillä. Oikein toimivat ohjelmat käyttävät mutexeja, atomisia operaatioita tai muuta synkronointia happens-before-suhteen luomiseksi jaetulle tilalle.