Zoeken…


Verplaats semantiek

Move-semantiek is een manier om het ene object naar het andere te verplaatsen in C ++. Hiervoor legen we het oude object en plaatsen we alles in het nieuwe object.

Hiervoor moeten we begrijpen wat een waardeverwijzing is. Een waardeverwijzing ( T&& waarbij T het objecttype is) verschilt niet veel van een normale verwijzing ( T& , nu lvalue-verwijzingen genoemd). Maar ze fungeren als 2 verschillende typen, en dus kunnen we constructors of functies maken die het ene of het andere type hebben, wat nodig zal zijn bij het omgaan met semantiek van bewegingen.

De reden waarom we twee verschillende types nodig hebben, is om twee verschillende gedragingen te specificeren. Lvalue-referentieconstructors zijn gerelateerd aan kopiëren, terwijl rvalue-referentieconstructors zijn gerelateerd aan verplaatsen.

Om een object te verplaatsen, gebruiken we std::move(obj) . Deze functie retourneert een waardeverwijzing naar het object, zodat we de gegevens van dat object in een nieuw kunnen stelen. Er zijn verschillende manieren om dit te doen, die hieronder worden besproken.

Belangrijk om op te merken is dat het gebruik van std::move slechts een waardeverwijzing creëert. Met andere woorden, de instructie std::move(obj) wijzigt de inhoud van obj niet, terwijl auto obj2 = std::move(obj) (mogelijk) dat wel doet.

Constructor verplaatsen

Stel dat we dit codefragment hebben.

class A {
public:
    int a;
    int b;
       
    A(const A &other) {
        this->a = other.a;
        this->b = other.b;
    }
};

Om een kopie-constructor te maken, dat wil zeggen om een functie te maken die een object kopieert en een nieuwe maakt, zouden we normaal gesproken de bovenstaande syntaxis kiezen, we zouden een constructor voor A hebben die een verwijzing naar een ander object van type A neemt, en we zouden het object handmatig binnen de methode kopiëren.

Als alternatief hadden we A(const A &) = default; die automatisch alle leden kopieert, gebruikmakend van de copyconstructor.

Om een constructor te maken, nemen we echter een rvalue-referentie in plaats van een lvalue-referentie, zoals hier.

class Wallet {
public:
    int nrOfDollars;
    
    Wallet() = default; //default ctor

    Wallet(Wallet &&other) {
        this->nrOfDollars = other.nrOfDollars;
        other.nrOfDollars = 0;
    }
};

Merk op dat we de oude waarden op zero . De standaard verplaatsingsconstructor ( Wallet(Wallet&&) = default; ) kopieert de waarde van nrOfDollars , aangezien het een POD is.

Aangezien semantiek van verplaatsing is ontworpen om de status 'stelen' van de oorspronkelijke instantie mogelijk te maken, is het belangrijk om na te denken over hoe de oorspronkelijke instantie er na deze diefstal zou moeten uitzien. In dit geval, als we de waarde niet naar nul zouden veranderen, zouden we het aantal dollars in het spel hebben verdubbeld.

Wallet a;
a.nrOfDollars = 1;
Wallet b (std::move(a)); //calling B(B&& other);
std::cout << a.nrOfDollars << std::endl; //0
std::cout << b.nrOfDollars << std::endl; //1

Dus hebben we een object geconstrueerd van een oude.


Hoewel het bovenstaande een eenvoudig voorbeeld is, laat het zien wat de constructor van de beweging moet doen. Het wordt nuttiger in complexere gevallen, zoals wanneer het om resourcebeheer gaat.

    // Manages operations involving a specified type.
    // Owns a helper on the heap, and one in its memory (presumably on the stack).
    // Both helpers are DefaultConstructible, CopyConstructible, and MoveConstructible.
    template<typename T,
             template<typename> typename HeapHelper,
             template<typename> typename StackHelper>
    class OperationsManager {
        using MyType = OperationsManager<T, HeapHelper, StackHelper>;

        HeapHelper<T>* h_helper;
        StackHelper<T> s_helper;
        // ...

      public:
        // Default constructor & Rule of Five.
        OperationsManager() : h_helper(new HeapHelper<T>) {}
        OperationsManager(const MyType& other)
          : h_helper(new HeapHelper<T>(*other.h_helper)), s_helper(other.s_helper) {}
        MyType& operator=(MyType copy) {
            swap(*this, copy);
            return *this;
        }
        ~OperationsManager() {
            if (h_helper) { delete h_helper; }
        }

        // Move constructor (without swap()).
        // Takes other's HeapHelper<T>*.
        // Takes other's StackHelper<T>, by forcing the use of StackHelper<T>'s move constructor.
        // Replaces other's HeapHelper<T>* with nullptr, to keep other from deleting our shiny
        //  new helper when it's destroyed.
        OperationsManager(MyType&& other) noexcept
          : h_helper(other.h_helper),
            s_helper(std::move(other.s_helper)) {
            other.h_helper = nullptr;
        }

        // Move constructor (with swap()).
        // Places our members in the condition we want other's to be in, then switches members
        //  with other.
        // OperationsManager(MyType&& other) noexcept : h_helper(nullptr) {
        //     swap(*this, other);
        // }

        // Copy/move helper.
        friend void swap(MyType& left, MyType& right) noexcept {
            std::swap(left.h_helper, right.h_helper);
            std::swap(left.s_helper, right.s_helper);
        }
    };

Opdracht verplaatsen

Net als hoe we een waarde aan een object met een waardeverwijzing kunnen toewijzen, door het te kopiëren, kunnen we ook de waarden van een object naar een ander verplaatsen zonder een nieuwe te construeren. We noemen deze verplaatsingsopdracht. We verplaatsen de waarden van het ene object naar een ander bestaand object.

Hiervoor moeten we operator = overbelasten, niet zodat er een waardeverwijzing nodig is, zoals in kopie-toewijzing, maar zodat er een waardeverwijzing nodig is.

class A {
    int a;
    A& operator= (A&& other) {
        this->a = other.a;
        other.a = 0;
        return *this;
    }
};

Dit is de typische syntaxis om verplaatsingstoewijzing te definiëren. We overbelasten operator = zodat we het een waardeverwijzing kunnen invoeren en het aan een ander object kunnen toewijzen.

A a;
a.a = 1;
A b;
b = std::move(a); //calling A& operator= (A&& other)
std::cout << a.a << std::endl; //0
std::cout << b.a << std::endl; //1

We kunnen dus een object toewijzen aan een ander.

Met std :: verplaatsen om de complexiteit te verminderen van O (n²) naar O (n)

C ++ 11 introduceerde kerntaal en standaard bibliotheekondersteuning voor het verplaatsen van een object. Het idee is dat wanneer een object o is een tijdelijke en men wil een logische kopie, dan is het veilig om gewoon pilfer o 's middelen, zoals een dynamisch toegewezen buffer, waardoor o logisch leeg maar nog steeds vernietigbare en kopieerbaar.

De ondersteuning van de kerntaal is hoofdzakelijk

  • het rvalue-referentietype builder && , bijv. std::string&& is een waardeverwijzing naar een std::string , waarmee wordt aangegeven dat dat object waarnaar wordt verwezen een tijdelijke is waarvan de bronnen zojuist kunnen worden gestapeld (dwz verplaatst)

  • speciale ondersteuning voor een constructor T( T&& ) verplaatsen , waarvan wordt verondersteld dat deze efficiënt middelen van het opgegeven andere object verplaatst, in plaats van deze middelen daadwerkelijk te kopiëren, en

  • speciale ondersteuning voor een operator voor verplaatsingstoepassing auto operator=(T&&) -> T& , die ook wordt verondersteld van de bron te worden verplaatst.

De standaard bibliotheekondersteuning is hoofdzakelijk de std::move functiesjabloon uit de <utility> header. Deze functie produceert een waardeverwijzing naar het opgegeven object, waarmee wordt aangegeven dat het kan worden verplaatst, alsof het een tijdelijk object is.


Voor een container is het feitelijke kopiëren typisch van O ( n ) complexiteit, waarbij n het aantal items in de container is, terwijl verplaatsen O (1) is, constante tijd. En voor een algoritme dat die container n keer logisch kopieert, kan dit de complexiteit verminderen van de meestal onpraktische O ( n ²) naar alleen lineaire O ( n ).

In zijn artikel 'Containers That Never Change' in Dr. Dobbs Journal in 19 september 2013 presenteerde Andrew Koenig een interessant voorbeeld van algoritmische inefficiëntie bij het gebruik van een programmeerstijl waarbij variabelen onveranderlijk zijn na initialisatie. Bij deze stijl worden lussen in het algemeen uitgedrukt met behulp van recursie. En voor sommige algoritmen zoals het genereren van een Collatz-reeks, vereist de recursie logisch het kopiëren van een container:

// Based on an example by Andrew Koenig in his Dr. Dobbs Journal article
// “Containers That Never Change” September 19, 2013, available at
// <url: http://www.drdobbs.com/cpp/containters-that-never-change/240161543>

// Includes here, e.g. <vector>

namespace my {
    template< class Item >
    using Vector_ = /* E.g. std::vector<Item> */;

    auto concat( Vector_<int> const& v, int const x )
        -> Vector_<int>
    {
        auto result{ v };
        result.push_back( x );
        return result;
    }

    auto collatz_aux( int const n, Vector_<int> const& result )
        -> Vector_<int>
    {
        if( n == 1 )
        {
            return result;
        }
        auto const new_result = concat( result, n );
        if( n % 2 == 0 )
        {
            return collatz_aux( n/2, new_result );
        }
        else
        {
            return collatz_aux( 3*n + 1, new_result );
        }
    }

    auto collatz( int const n )
        -> Vector_<int>
    {
        assert( n != 0 );
        return collatz_aux( n, Vector_<int>() );
    }
}  // namespace my

#include <iostream>
using namespace std;
auto main() -> int
{
    for( int const x : my::collatz( 42 ) )
    {
        cout << x << ' ';
    }
    cout << '\n';
}

Output:

42 21 64 32 16 8 4 2

Het aantal itemkopieerbewerkingen als gevolg van het kopiëren van de vectoren is hier ongeveer O ( ), omdat het de som 1 + 2 + 3 + ... n is .

In concrete getallen, met g ++ en Visual C ++ compilers collatz(42) resulteerde de bovenstaande aanroep van collatz(42) in een Collatz-reeks van 8 items en 36 collatz(42) (8 * collatz(42) = 28, plus enkele) in vectorkopieerconstructoroproepen.

Al deze itemkopieerbewerkingen kunnen worden verwijderd door eenvoudig vectoren te verplaatsen waarvan de waarden niet meer nodig zijn. Om dit te doen is het noodzakelijk om const en referentie voor de argumenten van het const te verwijderen, waarbij de vectoren op waarde worden doorgegeven. De functie-rendementen zijn al automatisch geoptimaliseerd. Voor de aanroepen waar vectoren worden doorgegeven en die later niet opnieuw in de functie worden gebruikt, past u gewoon std::move toe om die buffers te verplaatsen in plaats van ze daadwerkelijk te kopiëren:

using std::move;

auto concat( Vector_<int> v, int const x )
    -> Vector_<int>
{
    v.push_back( x );
    // warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
    // See https://stackoverflow.com/documentation/c%2b%2b/2489/copy-elision
    // return move( v );
    return v;
}

auto collatz_aux( int const n, Vector_<int> result )
    -> Vector_<int>
{
    if( n == 1 )
    {
        return result;
    }
    auto new_result = concat( move( result ), n );
    struct result;      // Make absolutely sure no use of `result` after this.
    if( n % 2 == 0 )
    {
        return collatz_aux( n/2, move( new_result ) );
    }
    else
    {
        return collatz_aux( 3*n + 1, move( new_result ) );
    }
}

auto collatz( int const n )
    -> Vector_<int>
{
    assert( n != 0 );
    return collatz_aux( n, Vector_<int>() );
}

Hier, met g ++ en Visual C ++ compilers, was het aantal itemkopieerbewerkingen als gevolg van vectorkopieerconstructoraanroepen exact 0.

Het algoritme is noodzakelijkerwijs nog steeds O ( n ) in de lengte van de geproduceerde Collatz-reeks, maar dit is een vrij dramatische verbetering: O ( n ²) → O ( n ).


Met enige taalondersteuning zou men misschien bewegend kunnen gebruiken en toch de onveranderlijkheid van een variabele tussen zijn initialisatie en laatste beweging kunnen uitdrukken en afdwingen, waarna elk gebruik van die variabele een fout zou moeten zijn. Helaas ondersteunt C ++ dat vanaf C ++ 14 niet. Voor lusvrije code kan het niet gebruiken na verplaatsing worden afgedwongen via een heraangifte van de relevante naam als een onvolledige struct , zoals met struct result; hierboven, maar dit is lelijk en zal waarschijnlijk niet worden begrepen door andere programmeurs; ook de diagnostiek kan behoorlijk misleidend zijn.

Samenvattend, de C ++ taal- en bibliotheekondersteuning voor verplaatsen maakt drastische verbeteringen in de complexiteit van algoritmen mogelijk, maar vanwege de onvolledigheid van de ondersteuning, ten koste van het verzaken van de garanties voor codecorrectheid en codehelderheid die const kan bieden.


Voor de volledigheid wordt de instrumentale vectorklasse gebruikt om het aantal itemkopieerbewerkingen als gevolg van invocaties van de kopieconstructor te meten:
template< class Item >
class Copy_tracking_vector
{
private:
    static auto n_copy_ops()
        -> int&
    {
        static int value;
        return value;
    }
    
    vector<Item>    items_;
    
public:
    static auto n() -> int { return n_copy_ops(); }

    void push_back( Item const& o ) { items_.push_back( o ); }
    auto begin() const { return items_.begin(); }
    auto end() const { return items_.end(); }

    Copy_tracking_vector(){}
    
    Copy_tracking_vector( Copy_tracking_vector const& other )
        : items_( other.items_ )
    { n_copy_ops() += items_.size(); }

    Copy_tracking_vector( Copy_tracking_vector&& other )
        : items_( move( other.items_ ) )
    {}
};

Gebruik semantiek verplaatsen op containers

U kunt een container verplaatsen in plaats van deze te kopiëren:

void print(const std::vector<int>& vec) {
    for (auto&& val : vec) {
        std::cout << val << ", ";
    }
    std::cout << std::endl;
}

int main() {
    // initialize vec1 with 1, 2, 3, 4 and vec2 as an empty vector
    std::vector<int> vec1{1, 2, 3, 4};
    std::vector<int> vec2;

    // The following line will print 1, 2, 3, 4
    print(vec1);

    // The following line will print a new line
    print(vec2);

    // The vector vec2 is assigned with move assingment.
    // This will "steal" the value of vec1 without copying it.
    vec2 = std::move(vec1);

    // Here the vec1 object is in an indeterminate state, but still valid.
    // The object vec1 is not destroyed,
    // but there's is no guarantees about what it contains.

    // The following line will print 1, 2, 3, 4
    print(vec2);
}

Gebruik een verplaatst object opnieuw

U kunt een verplaatst object opnieuw gebruiken:

void consumingFunction(std::vector<int> vec) {
    // Some operations
}

int main() {
    // initialize vec with 1, 2, 3, 4
    std::vector<int> vec{1, 2, 3, 4};

    // Send the vector by move
    consumingFunction(std::move(vec));

    // Here the vec object is in an indeterminate state.
    // Since the object is not destroyed, we can assign it a new content.
    // We will, in this case, assign an empty value to the vector,
    // making it effectively empty
    vec = {};

    // Since the vector as gained a determinate value, we can use it normally.
    vec.push_back(42);

    // Send the vector by move again.
    consumingFunction(std::move(vec));
}


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow