C++ on aina tuottanut nopeita ohjelmia. Valitettavasti C++11:een asti on ollut sitkeä syylä, joka hidastaa monia C++-ohjelmia: väliaikaisten objektien luominen. Joskus nämä väliaikaiset objektit voidaan optimoida pois kääntäjän toimesta (esimerkiksi paluuarvon optimointi). Näin ei kuitenkaan aina ole, ja se voi johtaa kalliisiin objektikopioihin. Mitä tarkoitan?

Asettakaamme, että sinulla on seuraava koodi:

#include <iostream>using namespace std;vector<int> doubleValues (const vector<int>& v){ vector<int> new_values; new_values.reserve(v.size()); for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr ) { new_values.push_back( 2 * *itr ); } return new_values;}int main(){ vector<int> v; for ( int i = 0; i < 100; i++ ) { v.push_back( i ); } v = doubleValues( v );}

Jos olet tehnyt paljon korkean suorituskyvyn töitä C++:lla, pahoittelen sen aiheuttamaa tuskaa. Jos et ole… No, käydäänpä läpi, miksi tämä koodi on kamalaa C++03-koodia. (Loppuosa tästä ohjeesta käsittelee sitä, miksi se on hyvää C++11-koodia.) Ongelma on kopioissa. Kun doubleValuesia kutsutaan, se muodostaa vektorin new_values ja täyttää sen. Pelkästään tämä ei ehkä ole ihanteellinen suorituskyky, mutta jos haluamme säilyttää alkuperäisen vektorin koskemattomana, tarvitsemme toisen kopion. Mutta mitä tapahtuu, kun painamme return-lauseen?

Koko new_valuesin sisältö on kopioitava! Periaatteessa tässä voisi olla jopa kaksi kopiota: yksi väliaikaiseen objektiin, joka palautetaan, ja toinen, kun vektorin osoitusoperaattori toimii rivillä v = doubleValues( v );. Kääntäjä saattaa optimoida ensimmäisen kopion automaattisesti pois, mutta ei voida välttää sitä, että osoituksessa v:lle joudutaan kopioimaan kaikki arvot uudestaan, mikä vaatii uuden muistin varauksen ja toisen iteraation koko vektorin yli.

Tämä esimerkki saattaa olla hieman keksitty – ja tietysti voit löytää keinoja välttää tämänkaltaiset ongelmat – esimerkiksi tallentamalla ja palauttamalla vektorin osoittimella tai antamalla vektorin täytettäväksi. Kumpikaan näistä ohjelmointityyleistä ei kuitenkaan ole erityisen luonnollinen. Lisäksi lähestymistapa, joka vaatii osoittimen palauttamista, on tuonut mukanaan ainakin yhden lisämuistiallokaation, ja yksi C++:n suunnittelutavoitteista on muistinallokaatioiden välttäminen.

Pahinta tässä koko tarinassa on se, että doubleValuesin palauttama objekti on väliaikainen arvo, jota ei enää tarvita. Kun sinulla on rivi v = doubleValues( v ), doubleValues( v ):n tulos vain heitetään pois, kun se on kopioitu! Teoriassa pitäisi olla mahdollista ohittaa koko kopiointi ja vain varastaa osoitin väliaikaisen vektorin sisältä ja pitää se v:ssä. Itse asiassa, miksi emme voi siirtää objektia? C++03:ssa vastaus oli, että ei ollut mitään tapaa sanoa, oliko objekti tilapäinen vai ei, vaan piti käyttää samaa koodia osoitusoperaattorissa tai kopiointikonstruktorissa riippumatta siitä, mistä arvo tuli, joten varastaminen ei ollut mahdollista. C++11:ssä vastaus on – voit!

Sitä varten on r-arvoviittaukset ja siirtosemantiikka! Siirtosemantiikan avulla voit välttää tarpeettomia kopioita, kun työskentelet väliaikaisten objektien kanssa, jotka ovat haihtumassa ja joiden resurssit voidaan turvallisesti ottaa tuolta väliaikaiselta objektilta ja käyttää toiseen.

Siirtosemantiikka perustuu C++11:n uuteen ominaisuuteen nimeltä rvalue-viittaukset,joka sinun on hyvä ymmärtää, jotta ymmärrät todella, mistä on kyse. Puhutaan siis ensin siitä, mikä on r-arvo, ja sitten siitä, mikä on r-arvoviittaus.Lopuksi palataan siirtosemantiikkaan ja siihen, miten se voidaan toteuttaa r-arvoviittausten avulla.

R-arvot ja l-arvot – katkeria kilpailijoita vai parhaita ystäviä?

C++:ssa on olemassa r-arvoja ja l-arvoja. Lvalue on lauseke, jonka osoitteen voi ottaa, locator-arvo–olemuksellisesti lvalue tarjoaa (puoli)pysyvän muistinpalasen. Voit tehdä osoituksia lvalueille. Esimerkki:

int a;a = 1; // here, a is an lvalue

Sinulla voi olla myös lvalues, jotka eivät ole muuttujia:

int x;int& getRef () { return x;}getRef() = 4;

Tässä tapauksessa getRef palauttaa viittauksen globaaliin muuttujaan, joten se palauttaa arvon, joka on tallennettu pysyvään paikkaan. (Voisit kirjaimellisesti kirjoittaa & getRef(), jos haluaisit, ja se antaisi sinulle x:n osoitteen.)

Rvalues ovat… no, rvalues eivät ole lvalues. Lauseke on r-arvo, jos sen tuloksena on väliaikainen objekti. Esimerkiksi:

int x;int getVal (){ return x;}getVal();

Tässä getVal() on r-arvo – palautettava arvo ei ole viittaus x:ään, se on vain väliaikainen arvo. Tilanne muuttuu hieman mielenkiintoisemmaksi, jos käytämme numeroiden sijasta todellisia objekteja:

string getName (){ return "Alex";}getName();

Tässä, getName palauttaa merkkijonon, joka muodostetaan funktion sisällä. Voit osoittaa getNamen tuloksen muuttujaan:

string name = getName();

Mutta osoitat tuloksen väliaikaisesta objektista, et jostain arvosta, jolla on kiinteä paikka. getName() on r-arvo.

Tilapäisten objektien havaitseminen r-arvoviittausten avulla

Tärkeää on se, että r-arvot viittaavat väliaikaisiin objekteihin – aivan kuten doubleValuesin palauttama arvo. Eikö olisi hienoa, jos voisimme ilman epäilyksen häivääkään tietää, että lausekkeen palauttama arvo on väliaikainen, ja jotenkin kirjoittaa koodia, joka on ylikuormitettu käyttäytymään eri tavalla väliaikaisten objektien suhteen? Kyllä, kyllä, todellakin olisi. Ja tätä varten rvalue-viittaukset ovat olemassa. Rvalue-viittaus on viittaus, joka sitoutuu vain väliaikaiseen objektiin. Mitä tarkoitan?

Ennen C++11:tä, jos sinulla oli tilapäinen objekti, voit käyttää ”tavallista” tai ”lvalue-viitettä” sitoaksesi sen, mutta vain jos se oli const:

const string& name = getName(); // okstring& name = getName(); // NOT ok

Intuitio tässä on se, että et voi käyttää ”mutable”-viitettä, koska jos käyttäisit sitä, pystyisit muokkaamaan jotakin objektia, joka on katoamaisillaan ja se olisi vaarallista. Huomaa muuten, että pysyvän viittauksen pitäminen väliaikaiseen objektiin varmistaa, että väliaikaista objektia ei tuhota välittömästi. Tämä on C++:n hieno tae, mutta se on silti väliaikainen objekti, joten et halua muuttaa sitä.

C++11:ssä on kuitenkin uudenlainen viittaus, ”rvalue-viittaus”, jonka avulla voit sitoa muuttuvan viittauksen rvalueeseen, mutta et lvalueeseen. Toisinsanoen, rvalue-viittaukset ovat täydellisiä havaitsemaan, onko arvo väliaikainen objekti vai ei. Rvalue-viittaukset käyttävät &&-syntaksia pelkän & sijasta, ja ne voivat olla const- ja non-const-viittauksia, aivan kuten lvalue-viittauksetkin, vaikka harvoin näkee const-rvalue-viittauksia (kuten tulemme näkemään, mutablereferenssit ovat tavallaan pointti):

const string&& name = getName(); // okstring&& name = getName(); // also ok - praise be!

Tämä kaikki on toistaiseksi hienoa ja hyvää, mutta miten siitä on apua? Tärkein asia lvalue-viittauksista vs. rvalue-viittauksista on se, mitä tapahtuu, kun kirjoitat funktioita, jotka ottavat argumentteina lvalue- tai rvalue-viittauksia. Sanotaan, että meillä on kaksi funktiota:

printReference (const String& str){ cout << str;}printReference (String&& str){ cout << str;}

Nyt käyttäytyminen muuttuu mielenkiintoiseksi – printReference-funktio, joka ottaaconst l-arvoviittauksen, hyväksyy minkä tahansa sille annetun argumentin, oli se sitten l- tai r-arvo, ja riippumatta siitä, onko l- tai r-arvo muuttuva vai ei. Toisen ylikuormituksen yhteydessä, kun printReference ottaa r-arvoviittauksen, sille annetaan kuitenkin kaikki arvot paitsi muuttuvat arvoviittaukset. Toisin sanoen, jos kirjoitat:

string me( "alex" );printReference( me ); // calls the first printReference function, taking an lvalue referenceprintReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference

Nyt meillä on tapa määrittää, viittaako viitemuuttuja väliaikaiseenobjektiin vai pysyvään objektiin. Menetelmän rvalue-viittausversio on kuin klubin salainen takaoven sisäänkäynti, jonne pääsee vain, jos on tilapäinen objekti (tylsä klubi, luulisin). Nyt kun meillä on menetelmämme, jolla voimme määrittää, oliko objekti väliaikainen vai pysyvä, miten voimme käyttää sitä?

Siirrä-konstruktori ja siirrä-toimitusoperaattori

Yleisin kuvio, jonka näet työskennellessäsi rvalue-viittausten kanssa, on luoda siirrä-konstruktori ja siirrä-toimitusoperaattori (joka noudattaa samoja periaatteita). Siirrä-konstruktori, kuten kopiointikonstruktori, ottaa argumenttinaan objektin instanssin ja luo uuden instanssin alkuperäisen objektin pohjalta. Move-konstruktori voi kuitenkin välttää muistin uudelleenjakamisen, koska tiedämme, että sille on annettu väliaikainen objekti, joten sen sijaan, että kopioisimme objektin kentät, siirrämme ne.

Mitä tarkoittaa objektin kentän siirtäminen? Jos kenttä on primitiivityyppi, kuten int, kopioimme sen vain. Tilanne muuttuu mielenkiintoisemmaksi, jos kenttä on osoitin: tällöin sen sijaan, että varaisimme ja alustaisimme uutta muistia, voimme yksinkertaisesti varastaa osoittimenja nollata osoittimen väliaikaisessa objektissa! Tiedämme, että väliaikaista objektia ei enää tarvita, joten voimme ottaa sen osoittimen pois sen alta.

Kuvitellaan, että meillä on yksinkertainen ArrayWrapper-luokka, kuten tämä:

class ArrayWrapper{ public: ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; } private: int *_p_vals; int _size;};

Huomaa, että kopiointikonstruktorin täytyy sekä varata muistia että kopioida jokainen arvo arraysta, yksi kerrallaan! Se on paljon työtä kopioinnille. Lisätään move-konstruktori ja saavutetaan massiivinen tehokkuus.

class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _size( 64 ) {} ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _size( other._size ) { other._p_vals = NULL; other._size = 0; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; int _size;};

Vau, move-konstruktori on itse asiassa yksinkertaisempi kuin kopiointikonstruktori!Se on melkoinen saavutus. Tärkeimmät huomioitavat asiat ovat:

  1. Parametri on ei-const rvalue-viittaus
  2. other._p_vals asetetaan NULL:ksi

Toinen havainto selittää ensimmäisen – emme voineet asettaa other._p_vals:ia NULL:ksi, jos olisimme ottaneet const rvalue-viittauksen. Mutta miksi meidän täytyy asettaaother._p_vals = NULL? Syy on destruktori – kun väliaikainen objekti poistuu toimialueeltaan, kuten kaikki muutkin C++:n objektit, sen destruktori suoritetaan.Kun sen destruktori suoritetaan, se vapauttaa _p_vals. Sama _p_vals, jonka juuri kopioimme! Jos emme aseta other._p_vals:n arvoksi NULL, siirto ei oikeastaan olisi siirto – se olisi vain kopiointi, joka aiheuttaa myöhemmin kaatumisen, kun alamme käyttää vapautettua muistia. Tämä on koko move-konstruktorin tarkoitus: välttää kopiointi muuttamalla alkuperäistä, väliaikaista objektia!

Jälleen kerran, ylikuormitussäännöt toimivat niin, että move-konstruktoria kutsutaan vain väliaikaista objektia varten–ja vain väliaikaista objektia, jota voidaan muuttaa. Tämä tarkoittaa sitä, että jos sinulla on funktio, joka palauttaa const-olion, se saa aikaan sen, että kopiointikonstruktori suoritetaan move-konstruktorin sijasta – älä siis kirjoita tällaista koodia:

const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!

On vielä yksi tilanne, jota emme ole käsitelleet, miten käsitellä amove-konstruktorissa – kun meillä on kenttä, joka on objekti. Kuvittele esimerkiksi, että sen sijaan, että meillä olisi size-kenttä, meillä olisi metadatakenttä, joka näyttäisi tältä:

class MetaData{public: MetaData (int size, const std::string& name) : _name( name ) , _size( size ) {} // copy constructor MetaData (const MetaData& other) : _name( other._name ) , _size( other._size ) {} // move constructor MetaData (MetaData&& other) : _name( other._name ) , _size( other._size ) {} std::string getName () const { return _name; } int getSize () const { return _size; } private: std::string _name; int _size;};

Nyt arrayllämme voi olla nimi ja koko, joten meidän on ehkä muutettava ArrayWrapperin määritelmää näin:

class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _metadata( 64, "ArrayWrapper" ) {} ArrayWrapper (int n) : _p_vals( new int ) , _metadata( n, "ArrayWrapper" ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { other._p_vals = NULL; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _metadata( other._metadata ) { for ( int i = 0; i < _metadata.getSize(); ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; MetaData _metadata;};

Toimiiko tämä? Eikö tunnukin hyvin luonnolliselta kutsua MetaDatamove-konstruktoria vain ArrayWrapperin move-konstruktorin sisältä? Ongelmana on, että tämä ei vain toimi. Syy on yksinkertainen: otherin arvo themove-konstruktorissa – se on r-arvoviittaus. Mutta r-arvoviittaus ei itse asiassa ole r-arvo. Se on l-arvo, ja siksi kutsutaan kopiointikonstruktoria, ei themove-konstruktoria. Tämä on outoa. Tiedän – se on hämmentävää. Tässä on tapa ajatella asiaa. R-arvo on lauseke, joka luo objektin, joka on aikeissa haihtua ilmaan. Se on viimeisillä jaloillaan elämässään – tai täyttämässä elämäntehtäväänsä. Yhtäkkiä siirretään väliaikainen move-konstruktoriin, ja se saa uuden elämän uudessa laajuudessa. Kontekstissa, jossa rvalue-lauseke evaluoitiin, väliaikainen objekti on todella ohi ja ohi. Mutta konstruktorissamme objektilla on nimi; se elää koko funktiomme ajan.Toisin sanoen, saatamme käyttää muuttujaa other useammin kuin kerran funktiossa,ja väliaikaisella objektilla on määritelty paikka, joka todella pysyy koko funktion ajan. Se on l-arvo termin locator value todellisessa merkityksessä,voimme paikantaa objektin tiettyyn osoitteeseen, joka pysyy vakaana funktiokutsun koko keston ajan. Saatamme itse asiassa haluta käyttää sitä myöhemmin funktiossa. Jos move-konstruktoria kutsuttaisiin aina, kun pidämme objektia rvalueviittauksessa, saattaisimme vahingossa käyttää siirrettyä objektia!

 // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { // if _metadata( other._metadata ) calls the move constructor, using // other._metadata here would be extremely dangerous! other._p_vals = NULL; }

Viimeinen tapa: sekä lvalue- että rvalue-viittaukset ovat lvalue-ilmaisuja. Ero on siinä, että lvalue-viittauksen täytyy olla const pitääkseen sisällään viittauksenrvalueeseen, kun taas rvalue-viittaus voi aina pitää sisällään viittauksen rvalueeseen.Se on kuin ero osoittimen ja sen välillä, mihin osoitetaan. Asia, johon osoitetaan, tuli rvalueesta, mutta kun käytämme rvalue-viittausta itsessään, tuloksena on lvalue.

std::move

Mikä on siis tämän tapauksen käsittelyn juju? Meidän täytyy käyttää std::movea, from<utility>–std::move on tapa sanoa: ”ok, rehellisesti sanottuna tiedän, että minulla on l-arvo, mutta haluan sen olevan r-arvo.” std::move ei sinänsä siirrä mitään; se vain muuttaa l-arvon r-arvoksi, jotta voit kutsua move-konstruktoria. Koodimme pitäisi näyttää tältä:

#include <utility> // for std::move // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( std::move( other._metadata ) ) { other._p_vals = NULL; }

Ja tietysti meidän pitäisi tosiaan palata MetaDataan ja korjata sen oma move-konstruktori niin, että se käyttää std::movea hallussaan pitämäänsä merkkijonoon:

 MetaData (MetaData&& other) : _name( std::move( other._name ) ) // oh, blissful efficiency : _size( other._size ) {}

Liikkeen osoitusoperaattori

Niin kuin meillä on move-konstruktori, meillä pitäisi olla myös move-operaattori. Voit helposti kirjoittaa sellaisen käyttämällä samoja tekniikoita kuin move-konstruktorin luomisessa.

Liike-konstruktorit ja implisiittisesti generoidut konstruktorit

Kuten tiedät, C++:ssa kun julistat minkä tahansa konstruktorin, kääntäjä ei enää generoi oletuskonstruktoria puolestasi. Sama pätee tässäkin: siirtokonstruktorin lisääminen luokkaan edellyttää, että ilmoitat ja määrittelet oman oletuskonstruktorisi. Toisaalta move-konstruktorin ilmoittaminen ei estä kääntäjää tarjoamasta implisiittisesti generoitua kopiokonstruktoria, eikä move-luokitusoperaattorin ilmoittaminen estä vakiomuotoisen luokitusoperaattorin luomista.

Miten std::move toimii

Voi olla, että ihmettelet, miten kirjoitetaan funktio kuten std::move? Miten saatte tämän maagisen ominaisuuden muuttaa l-arvon arvoviitteeksi? Vastaus, kuten arvata saattaa, on tyypitys. Varsinainen std::move-ilmoitus on hieman monimutkaisempi, mutta pohjimmiltaan se on vain staattinen_cast r-arvoviittaukseen. Tämä tarkoittaa itse asiassa sitä, että sinun ei oikeastaan tarvitse käyttää movea – mutta sinun pitäisi, koska on paljon selkeämpää, mitä tarkoitat. Se, että castia tarvitaan, on muuten erittäin hyvä asia! Se tarkoittaa, että et voi vahingossa muuntaa l-arvoa r-arvoksi, mikä olisi vaarallista, koska se voisi mahdollistaa vahingossa tapahtuvan siirron. Sinun täytyy eksplisiittisesti käyttää std::movea (tai castia) muuttaaksesi l-arvon r-arvoviitteeksi, eikä r-arvoviite koskaan sitoudu omaan l-arvoonsa.

Eksplisiittisen r-arvoviitteen palauttaminen funktiosta

Onko koskaan tilanteita, joissa sinun pitäisi kirjoittaa funktio, joka palauttaa r-arvoviitteen? Mitä rvalue-viittauksen palauttaminen ylipäätään tarkoittaa? Eivätkö funktiot, jotka palauttavat objekteja arvon mukaan, ole jo r-arvoja?

Vastataan ensin toiseen kysymykseen: eksplisiittisen r-arvoviittauksen palauttaminen on eri asia kuin objektin palauttaminen arvon mukaan. Otetaan seuraava yksinkertainen esimerkki:

int x;int getInt (){ return x;}int && getRvalueInt (){ // notice that it's fine to move a primitive type--remember, std::move is just a cast return std::move( x );}

Selvästi ensimmäisessä tapauksessa, huolimatta siitä, että getInt() on r-arvo, tehdään kopio muuttujasta x. Voimme jopa nähdä tämän kirjoittamalla pienen apufunktion:

void printAddress (const int& v) // const ref to allow binding to rvalues{ cout << reinterpret_cast<const void*>( & v ) << endl;}printAddress( getInt() ); printAddress( x );

Kun suoritat tämän ohjelman, näet, että tulostuu kaksi erillistä arvoa.

Toisaalta

printAddress( getRvalueInt() ); printAddress( x );

tulostaa saman arvon, koska palautamme tässä nimenomaisesti r-arvon.

Siten r-arvoviittauksen palauttaminen on eri asia kuin r-arvoviittauksen palauttamatta jättäminen, mutta tämä ero ilmenee huomattavimmin, jos sinulla on jo olemassa oleva objekti, jonka olet palauttamassa funktiossa luodun väliaikaisen objektin sijaan (jolloin kääntäjä todennäköisesti poistaa kopioinnin puolestasi).

Kysymys siitä, haluatko tehdä näin. Vastaus on:todennäköisesti ei. Useimmissa tapauksissa se vain tekee todennäköisemmäksi sen, että päädyt roikkuvaan viittaukseen (tapaus, jossa viittaus on olemassa, mutta väliaikainenobjekti, johon se viittaa, on tuhottu). Ongelma on melko samanlainen kuin lvalue-viittauksen palauttamisen vaara – objektia, johon viitataan, ei ehkä enää ole olemassa. R-arvoviittaukset eivät voi maagisesti pitää objektia elossa puolestasi. r-arvoviittauksen palauttaminen olisi järkevää lähinnä hyvin harvinaisissa tapauksissa, joissa sinulla on jäsenfunktio ja sinun on palautettava tulos, joka saadaan kutsumallastd::move luokan kenttään kyseisestä funktiosta – ja kuinka usein aiot tehdä niin?

Liikesemantiikka ja standardikirjasto

Palatakseni alkuperäiseen esimerkkiin– käytimme vektoria, emmekä voi kontrolloida vektoriluokkaa ja sitä, onko sillä move-konstruktori-move-toimitusoperaattori vai ei. Onneksi standardointikomitea on viisas, ja move-semantiikka on lisätty standardikirjastoon. Tämä tarkoittaa, että voit nyt tehokkaasti palauttaa vektoreita, karttoja, merkkijonoja ja mitä tahansa muita standardikirjasto-objekteja, joita haluat, hyödyntäen move-semantiikkaa.

Liikuteltavat objektit STL:n säiliöissä

Vakiokirjastossa mennään vielä askeleen pidemmälle. Jos otat movesemantiikan käyttöön omissa objekteissasi luomalla move-toimitusoperaattoreita ja move-konstruktoreita, kun tallennat näitä objekteja konttiin, STL käyttää automaattisesti std::movea hyödyntäen automaattisesti move-toimintoa sallivia luokkia tehottomien kopioiden eliminoimiseksi.

Articles

Vastaa

Sähköpostiosoitettasi ei julkaista.