Hareket semantiği nedir?


1702

Scott Meyers ile C ++ 0x ile ilgili Yazılım Mühendisliği radyo podcast röportajını dinledim . Yeni özelliklerin çoğu bana mantıklı geldi ve aslında bir tane hariç, C ++ 0x hakkında heyecanlıyım. Hala hareket semantiği almıyorum ... Tam olarak nedir?


20
C ve C ++ 'daki lvalues ve rvalues hakkında [Eli Bendersky'nin blog makalesi] ( eli.thegreenplace.net/2011/12/15/… ) buldum . Ayrıca C ++ 11'de rvalue referanslarından bahsediyor ve küçük örneklerle tanıştırıyor.
Nils


19
Her yıl ya da öylesine merak ediyorum "yeni" hareket semantik C ++ ile ilgili, ben google ve bu sayfaya almak. Yanıtları okudum, beynim kapanıyor. C'ye geri dönüyorum ve her şeyi unutuyorum! Ben çıkmaza girmiştim.
gökyüzü

7
@sky std :: vector <> düşünün ... Orada bir yerde öbek üzerinde bir dizi için bir işaretçi var. Bu nesneyi kopyalarsanız, yeni bir arabellek ayrılmalıdır ve arabellekteki verilerin yeni arabelleğe kopyalanması gerekir. İşaretçiyi çalmanın uygun olacağı herhangi bir durum var mı? Derleyici nesnenin geçici olduğunu bildiğinde, cevap EVET'tir. Semantik taşıma, derleyici taşındığınız nesnenin gitmek üzere olduğunu bildiğinde sınıflarınızın bağırsaklarının nasıl farklı bir nesneye taşınabileceğini ve bırakılabileceğini tanımlamanızı sağlar.
dicroce

Anlayabildiğim tek referans: learncpp.com/cpp-tutorial/… , yani hareket semantiğinin orijinal mantığı akıllı işaretçilerden.
jw_

Yanıtlar:


2481

Ben hareket semantiği örnek kod ile anlamak en kolay buluyorum. Sadece yığınla ayrılmış bellek bloğuna bir işaretçi tutan çok basit bir dize sınıfıyla başlayalım:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Hafızayı kendimiz yönetmeyi seçtiğimiz için , üç kuralı takip etmeliyiz . Ödev operatörünün yazılmasını erteleyeceğim ve şimdilik yıkıcı ve kopya yapıcıyı uygulayacağım:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Copy yapıcısı, dize nesnelerini kopyalamanın ne anlama geldiğini tanımlar. Parametre const string& that, aşağıdaki örneklerde kopyalar oluşturmanıza izin veren tür dizesinin tüm ifadelerine bağlanır:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Şimdi, hareket semantiğine ilişkin temel bilgiler geliyor. Sadece kopyaladığımız ilk satırda xbu derin kopyanın gerçekten gerekli olduğunu unutmayın, çünkü xdaha sonra incelemek isteyebiliriz ve bir xşekilde değişmiş olsaydı çok şaşırırdık. xÜç kez nasıl söylediğimi fark ettiniz mi (bu cümleyi eklerseniz dört kez) ve her seferinde aynı nesneyi kastettiniz mi? Biz buna x"değer" gibi ifadeler diyoruz .

Satır 2 ve 3'teki bağımsız değişkenler değer değil, değerlerdir, çünkü alttaki dize nesnelerinin adı yoktur, bu nedenle istemcinin bunları daha sonraki bir zamanda yeniden denetleme yolu yoktur. rvalues, sonraki noktalı virgülde imha edilen geçici nesneleri belirtir (daha kesin olmak gerekirse, rvalülayı sözlüksel olarak içeren tam ifadenin sonunda). Bunun nedeni başlatılması sırasında önemlidir bve cbiz kaynak dizesi ile istedik ve ne yapabildiğimiz, istemci bir fark anlayamadı !

C ++ 0x, diğer şeylerin yanı sıra, işlev aşırı yüklenmesi yoluyla değer argümanlarını tespit etmemizi sağlayan "değer referansı" adlı yeni bir mekanizma sunar. Tek yapmamız gereken bir rvalue referans parametresi olan bir yapıcı yazmak. Bu yapıcı İçinde yapabileceğimiz istediğimiz her şeyi biz bırakın sürece gibi kaynakla bazı geçerli duruma:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Burada ne yaptık? Yığın verilerini derinden kopyalamak yerine, işaretçiyi kopyaladık ve sonra orijinal işaretçiyi null değerine ayarladık ('kaynak nesnenin yıkıcısından' sil [] 'nin' sadece çalınan verilerimizi 'serbest bırakmasını önlemek için). Aslında, başlangıçta kaynak dizgiye ait olan verileri "çaldık". Yine, anahtar görüş, müşterinin hiçbir koşulda kaynağın değiştirildiğini tespit edememesidir. Burada gerçekten bir kopya yapmadığımızdan, bu kurucuya "hareket kurucu" diyoruz. Görevi, kaynakları kopyalamak yerine bir nesneden diğerine taşımaktır.

Tebrikler, artık hareket semantiğinin temellerini anlıyorsunuz! Ödev operatörünü uygulayarak devam edelim. Kopyalama ve takas deyimine aşina değilseniz , öğrenin ve geri gelin, çünkü bu istisna güvenliği ile ilgili harika bir C ++ deyimidir.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Ha, bu mu? "Değer referansı nerede?" sorabilirsiniz. "Burada ihtiyacımız yok!" cevabım :)

Parametreyi that değere göre geçirdiğimizi unutmayın , bu yüzden thatdiğer tüm dize nesneleri gibi başlatılmalıdır. Tam olarak nasıl thatbaşlatılacak? C ++ 98'in eski günlerinde , cevap "kopya yapıcı tarafından" olurdu. C ++ 0x, derleyici, atama işleci bağımsız değişkeninin bir değer veya bir değer olup olmadığına bağlı olarak kopya yapıcısı ve taşıma yapıcısı arasında seçim yapar.

Bu nedenle a = b, kopya oluşturucu başlatılır that(ifade bir değer bolduğu için) ve atama operatörü içeriği yeni oluşturulmuş derin bir kopyayla değiştirir. Bu, kopyalama ve takas deyiminin tam tanımıdır - bir kopya oluşturun, içeriği kopyayla değiştirin ve daha sonra kapsamı bırakarak kopyadan kurtulun. Burada yeni bir şey yok.

Eğer söylersen aynı a = x + y, hareket yapıcı başlatır that(ifade, çünkü x + ybir rvalue), böylece dahil hiçbir derin kopyalama, sadece verimli bir hareket vardır. thatargümandan hala bağımsız bir nesnedir, ancak yığın verilerinin kopyalanması gerekmediği için taşınması önemsizdi. Kopyalamak gerekli değildi çünkü x + ybir değerdir ve yine, değerlerle gösterilen dize nesnelerinden taşınmak uygundur.

Özetlemek gerekirse, kopya yapıcı derin bir kopya oluşturur, çünkü kaynağa dokunulmamalıdır. Öte yandan, hareket yapıcı sadece işaretçiyi kopyalayıp kaynaktaki işaretçiyi null değerine ayarlayabilir. Kaynak nesneyi bu şekilde "geçersiz kılmak" uygundur, çünkü istemcinin nesneyi yeniden denetleme yolu yoktur.

Umarım bu örnek ana noktayı ele alır. Değerleri basitleştirmek ve kasıtlı olarak basit tutmak için semantikleri taşımak için çok daha fazlası var. Daha fazla ayrıntı istiyorsanız lütfen ek cevabımı inceleyin .


40
@Ama benim ctor daha sonra asla kullanılamaz bir rvalue alıyorsanız, neden tutarlı / güvenli bir durumda bırakmak rahatsız bile gerekir? Bunu.data = 0 olarak ayarlamak yerine, neden sadece bırakmıyorsunuz?
einpoklum

70
@einpoklum Çünkü olmadan that.data = 0, karakterler çok erken (geçici olarak öldüğü zaman) ve ayrıca iki kez yok edilirdi . Verileri çalmak istiyorsunuz, paylaşmayın!
fredoverflow

19
@ einpoklum Düzenli olarak programlanan yıkıcı hala çalıştırılır, bu nedenle kaynak nesnenin hareket sonrası durumunun çökmeye neden olmadığından emin olmanız gerekir. Daha iyisi, kaynak nesnenin bir ödevin veya başka bir yazmanın alıcısı olabileceğinden emin olmalısınız.
CTMacUser

12
@pranitkothari Evet, tüm nesneler yok edilmeli, hatta nesnelerden taşınmalıdır. Bu gerçekleştiğinde char dizisinin silinmesini istemediğimizden, işaretçiyi null değerine ayarlamamız gerekir.
fredoverflow

7
@ Nullptr delete[]üzerindeki Virus721, C ++ standardı tarafından no-op olarak tanımlanır.
fredoverflow

1057

İlk cevabım, anlambilimi taşımak için son derece basitleştirilmiş bir girişti ve basit tutmak için birçok ayrıntı dışarıda bırakıldı. Ancak, semantiği taşımak için çok daha fazlası var ve boşlukları doldurmak için ikinci bir cevap vermenin zamanının geldiğini düşündüm. İlk cevap zaten oldukça eski ve bunu tamamen farklı bir metinle değiştirmek doğru gelmiyordu. Bence hala ilk giriş olarak hizmet ediyor. Ama daha derine inmek istiyorsanız, okuyun :)

Stephan T. Lavavej değerli geri bildirimler sağlamak için zaman ayırdı. Çok teşekkür ederim, Stephan!

Giriş

Anlambilimi taşıma, bir nesnenin belirli koşullar altında başka bir nesnenin dış kaynaklarının sahipliğini almasına izin verir. Bu iki şekilde önemlidir:

  1. Pahalı kopyaları ucuz hamlelere dönüştürmek. Bir örnek için ilk cevabıma bakın. Bir nesne en az bir harici kaynağı (doğrudan veya dolaylı olarak üye nesneleri aracılığıyla) yönetmezse, taşıma semantiğinin kopya semantiğine göre herhangi bir avantaj sağlamayacağını unutmayın. Bu durumda, bir nesneyi kopyalamak ve bir nesneyi taşımak aynı şey anlamına gelir:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. Güvenli "sadece hareket" türlerinin uygulanması; yani, kopyalamanın mantıklı olmadığı, ancak taşındığı türler. Örnekler arasında kilitler, dosya tanıtıcıları ve benzersiz sahiplik semantiği olan akıllı işaretçiler bulunur. Not: Bu cevap std::auto_ptr, std::unique_ptrC ++ 11 ile değiştirilen kullanımdan kaldırılmış bir C ++ 98 standart kitaplık şablonunu tartışır . Ara C ++ programcıları muhtemelen en azından biraz tanıdıktır std::auto_ptrve "hareket semantiği" nedeniyle, C ++ 11'deki hareket semantiğini tartışmak için iyi bir başlangıç ​​noktası gibi görünüyor. YMMV.

Hareket nedir?

C ++ 98 standart kitaplığı, benzersiz sahiplik semantiği adı verilen akıllı bir işaretçi sunar std::auto_ptr<T>. Eğer aşina değilseniz auto_ptr, amacı dinamik olarak tahsis edilmiş bir nesnenin istisnalar karşısında bile her zaman serbest bırakılmasını garanti etmektir:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

Hakkında olağandışı olan şey auto_ptr"kopyalama" davranışı:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Başlatma nasıl Not bile ayok değil bunun yerine üçgen kopyalamak, ancak gelen üçgenin sahipliğini aktarır aiçin b. Ayrıca "demek aolduğunu taşındı b veya" üçgen olan " taşındı gelen a etmek b ". Bu kafa karıştırıcı gelebilir, çünkü üçgenin kendisi her zaman bellekte aynı yerde kalır.

Bir nesneyi taşımak, yönettiği bir kaynağın sahipliğini başka bir nesneye aktarmak anlamına gelir.

Kopya oluşturucu auto_ptrmuhtemelen böyle bir şeye benziyor (biraz basitleştirilmiş):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Tehlikeli ve zararsız hamleler

Tehlikeli olan şey auto_ptr, sözdizimsel olarak bir kopyaya benzeyen şeyin aslında bir hareket olmasıdır. Taşınan bir konumdan bir üye işlevini çağırmaya çalışmak, auto_ptrtanımlanmamış davranışı başlatır, bu nedenle bir auto_ptrtaşındıktan sonra kullanmamaya çok dikkat etmelisiniz :

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Ancak her zaman tehlikeli auto_ptrdeğildir . Fabrika fonksiyonları aşağıdakiler için mükemmel bir kullanım durumudur :auto_ptr

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Her iki örneğin de aynı sözdizimsel kalıbı nasıl izlediğine dikkat edin:

auto_ptr<Shape> variable(expression);
double area = expression->area();

Ve yine de, bunlardan biri tanımsız davranışlar çağrısındayken, diğeri tanımıyor. Yani ifadeleri arasındaki fark nedir ave make_triangle()? İkisi de aynı tipte değil mi? Gerçekten de öyle, ama farklı değer kategorileri var .

Değer kategorileri

Açıkçası, abir auto_ptrdeğişkeni ifade make_triangle()eden ifade ile bir auto_ptrby değeri döndüren bir fonksiyonun çağrısını ifade eden ve böylece auto_ptrher çağrıldığında yeni bir geçici nesne oluşturan ifade arasında derin bir fark olmalıdır . abir değerlik örneğidir , oysa make_triangle()bir değerlik örneğidir .

Böyle bir değerden hareket etmek atehlikelidir, çünkü daha sonra bir üye işlevini atanımlanamayan davranışı çağırarak arayabiliriz . Öte yandan, make_triangle()kopya oluşturucu işini yaptıktan sonra, geçici olanı tekrar kullanamayız gibi, değerlerden hareket etmek tamamen güvenlidir. Söz konusu geçiciliği ifade eden bir ifade yoktur; sadece make_triangle()tekrar yazarsak, farklı bir geçici alırız . Aslında, geçici taşındı zaten bir sonraki satıra gitti:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Harflerin lve rödevin sol tarafında ve sağ tarafında tarihi bir kökene sahip olduğunu unutmayın . Bu artık C ++ için geçerli değildir, çünkü bir ödevin sol tarafında görünmeyen değerler vardır (atama operatörü olmayan diziler veya kullanıcı tanımlı türler gibi) ve (sınıf türlerinin tüm değerlerinin) bir atama operatörü ile).

Sınıf türü değeri, değerlendirmesi geçici bir nesne oluşturan bir ifadedir. Normal şartlar altında, aynı kapsamdaki başka hiçbir ifade aynı geçici nesneyi göstermez.

Değer referansları

Artık değerlerden hareket etmenin potansiyel olarak tehlikeli olduğunu, ancak değerlerden hareket etmenin zararsız olduğunu anlıyoruz. C ++, değer bağımsız değişkenlerini değer bağımsız değişkenlerinden ayırmak için dil desteğine sahip olsaydı, ya artık değer değerlerinden hareket etmeyi tamamen yasaklayabilir ya da en azından çağrı sitesinde açık değerlerden hareket ettirebiliriz, böylece artık kazayla hareket etmeyiz.

C ++ 11'in bu soruna cevabı rvalue referanslarıdır . Bir rvalue referansı, yalnızca rvalue'lara bağlanan ve sözdizimi olan yeni bir referans türüdür X&&. Eski iyi referans X&artık bir değer referansı olarak bilinir . (Not X&&olan olmayan bir referans referans; böyle bir şey C orada ++).

Biz atarsak constkarışımı içine, zaten referanslardan dört farklı tür var. Ne tür ifadelere Xbağlanabilirler?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

Pratikte unutabilirsiniz const X&&. Gelirleri okumakla sınırlı olmak pek kullanışlı değil.

Bir rvalue referansı X&&, yalnızca rvalue'lara bağlanan yeni bir referans türüdür.

Örtük dönüşümler

Rvalue referansları çeşitli versiyonlardan geçti. Sürüm 2.1, bir rvalue referans yana X&&aynı zamanda farklı türdeki tüm değer kategorilerine bağlanan Y, bir kapalı dönüştürme olması koşuluyla Yiçin X. Bu durumda, geçici bir tür Xoluşturulur ve rvalue referansı bu geçici dosyaya bağlanır:

void some_function(std::string&& r);

some_function("hello world");

Yukarıdaki örnekte, "hello world"bir tür değeridir const char[12]. Üzerinden const char[12]sonuna örtülü bir dönüşüm const char*olduğundan std::string, geçici bir tür std::stringoluşturulur ve rbu geçici öğeye bağlanır. Bu, değerler (ifadeler) ve geçiciler (nesneler) arasındaki ayrımın biraz bulanık olduğu durumlardan biridir.

Yapıcıları taşıma

X&&Parametreli bir işleve yararlı bir örnek , hareket yapıcıdır X::X(X&& source) . Amacı, yönetilen kaynağın sahipliğini kaynaktan mevcut nesneye aktarmaktır.

C ++ 11'de, rvalue referanslarından yararlanan std::auto_ptr<T>yerini aldı std::unique_ptr<T>. İçin basitleştirilmiş bir sürümünü geliştirip tartışacağım unique_ptr. İlk olarak, ham bir işaretçiyi kapsülleyip operatörleri aşırı yüklüyoruz ->ve *böylece sınıfımız bir işaretçi gibi geliyor:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Yapıcı nesnenin sahipliğini alır ve yıkıcı onu siler:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Şimdi ilginç kısım, hareket oluşturucu geliyor:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Bu hareket yapıcı, auto_ptrkopya oluşturucunun yaptıklarını aynen yapar , ancak yalnızca değerlerle birlikte sağlanabilir:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

İkinci satır derlenemez, çünkü abir değerdir, ancak parametre unique_ptr&& sourceyalnızca değerlere bağlanabilir. Bu tam olarak istediğimiz şeydi; tehlikeli hamleler asla örtük olmamalıdır. Üçüncü satır sadece iyi derler, çünkü make_triangle()bir değerdir. Hareket yapıcı geçici Varış sahipliğini aktaracaktır c. Yine, tam olarak istediğimiz buydu.

Taşıma yapıcısı, yönetilen bir kaynağın sahipliğini geçerli nesneye aktarır.

Atama işleçlerini taşıma

Son eksik parça taşıma atama operatörüdür. Görevi eski kaynağı serbest bırakmak ve yeni kaynağı argümanından elde etmektir:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Taşıma atama operatörünün bu uygulamasının, hem yıkıcı hem de taşıma yapıcısının mantığını nasıl kopyaladığını unutmayın. Kopyala ve takas deyimine aşina mısınız? Semantiği taşımak ve takas deyimi olarak taşımak için de uygulanabilir:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Şimdi bu sourcebir tür değişkeni unique_ptr, hareket yapıcısı tarafından başlatılacak; yani, argüman parametreye taşınacaktır. Hareket yapıcısının kendisinde bir değer referans parametresi bulunduğundan, bağımsız değişken yine de bir değer olmalıdır. Kontrol akış kapanış ayracı ulaştığında operator=, sourceotomatik olarak eski kaynak bırakmadan, kapsam dışına gider.

Taşıma atama operatörü, yönetilen bir kaynağın sahipliğini geçerli nesneye aktarır ve eski kaynağı serbest bırakır. Taşıma ve değiştirme deyimi uygulamayı basitleştirir.

Değerlerden hareket

Bazen, değerlerden hareket etmek isteriz. Yani, bazen derleyiciye bir lvalue'yu bir rvalue gibi muamele etmesini isteriz, böylece potansiyel olarak güvenli olmasa bile, hareket yapıcısını çağırabilir. Bu amaçla, C ++ 11 std::moveüstbilgi adı verilen standart bir kütüphane işlev şablonu sunar <utility>. Bu isim biraz talihsizdir, çünkü std::movebasitçe bir rvalueya bir lvalue verir; o mu değil kendisi tarafından bir şey taşıyın. Sadece hareket etmeyi sağlar . Belki de adlandırılmış olması gerekirdi std::cast_to_rvalueya std::enable_move, ama biz artık adı ile sıkışmış.

Bir lvalue'dan açıkça nasıl hareket edeceğiniz aşağıda açıklanmıştır:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Üçüncü satırdan sonra aartık bir üçgen olmadığını unutmayın. Tarafından Çünkü yolunda, açıkça yazma std::move(a), biz niyetlerimizi yapılan net: "Eğer ne istersen Değerli yapıcı yapmak abaşlatmak için c, ben umurumda değil aartık ile yol var çekinmeyin. a".

std::move(some_lvalue) bir rvalue bir rvalue için döküm, böylece sonraki bir hareket sağlar.

XValues

Olsa Not std::move(a)bir rvalue ise, kendi değerlendirme yok değil geçici bir nesne oluşturmak. Bu muamele komiteyi üçüncü bir değer kategorisi oluşturmaya zorladı. Geleneksel anlamda bir rvalue olmasa da, bir rvalue referansına bağlanabilecek bir şeye xvalue (eXpiring değeri) denir . Geleneksel SağDeğerler için yeniden adlandırıldı prvalues (Saf SağDeğerler).

Hem değerler hem de x değerler değerlerdir. X değerleri ve lvalues ​​her ikisi de glvalues (Genelleştirilmiş lvalues ). İlişkilerin bir diyagramla kavranması daha kolaydır:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Sadece xvalues ​​gerçekten yeni; geri kalanı sadece yeniden adlandırma ve gruplama nedeniyle.

C ++ 98 değerleri, C ++ 11'deki değerler olarak bilinir. Önceki paragraflardaki "rvalue" tüm tekrarlarını zihinsel olarak "prevalue" ile değiştirin.

İşlevlerden çıkma

Şimdiye kadar, yerel değişkenlere ve fonksiyon parametrelerine doğru hareket gördük. Ancak ters yönde hareket etmek de mümkündür. Bir işlev değere göre döndürülürse, çağrı sitesindeki bazı nesneler (muhtemelen yerel bir değişken veya geçici, ancak herhangi bir nesne olabilir) ifade returnkurucusundan bağımsız değişken olarak ifadeden sonra ifadeyle başlatılır :

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Belki de şaşırtıcı bir şekilde, otomatik nesneler (olarak bildirilmeyen yerel değişkenler static) de örtük olarak işlevlerin dışına taşınabilir:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Nasıl hamle yapıcısı değeri resultbir argüman olarak kabul eder ? Kapsamı resultsona ermek üzeredir ve yığın çözme sırasında yok edilecektir. Daha sonra kimse bunun bir resultşekilde değiştiğinden şikayet edemezdi ; kontrol akışı arayan kişiye geri döndüğünde, resultartık mevcut değildir! Bu nedenle, C ++ 11, otomatik nesnelerin yazmak zorunda kalmadan işlevlerden döndürülmesini sağlayan özel bir kurala sahiptir std::move. Aslında, "adlandırılmış dönüş değeri optimizasyonu" nu (NRVO) engellediğinden, otomatik nesneleri işlevlerin dışına taşımak için asla kullanmamalısınız std::move.

std::moveOtomatik nesneleri işlevlerin dışına taşımak için asla kullanmayın .

Her iki fabrika işlevinde de dönüş türünün bir değer referansı değil, bir değer olduğunu unutmayın. Rvalue referansları hala referanstır ve her zaman olduğu gibi hiçbir zaman otomatik bir nesneye başvuru göndermemelisiniz; derleyiciyi kodunuzu kabul etmesi için kandırırsanız, arayan sarkan bir referansla sonuçlanır:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Otomatik nesneleri asla rvalue referansı ile döndürmeyin. Taşınma, sadece bir yapıyı std::movebir rvalue referansına bağlayarak değil, yalnızca yapıcı tarafından gerçekleştirilir .

Üyelere geçiş

Er ya da geç, böyle bir kod yazacaksınız:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Temel olarak, derleyici parameterbir değer olduğunu şikayet edecektir . Türüne bakarsanız, bir değer referansı görürsünüz, ancak bir değer referansı basitçe "bir değere bağlı bir referans" anlamına gelir; o mu değil referans kendisi rvalue olduğu anlamına! Gerçekten de, parametersadece adıyla sıradan bir değişkendir. parameterYapıcı gövdesinin içinde istediğiniz sıklıkta kullanabilirsiniz ve her zaman aynı nesneyi gösterir. Bundan dolaylı olarak hareket etmek tehlikeli olurdu, bu nedenle dil onu yasaklar.

Adlandırılmış bir değer referansı, diğer tüm değişkenler gibi bir değerdir.

Çözüm, hareketi manuel olarak etkinleştirmektir:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Bunun parameterbaşlangıcından sonra artık kullanılmadığını iddia edebilirsiniz member. Neden std::movedönüş değerlerinde olduğu gibi sessizce eklemek için özel bir kural yok ? Muhtemelen derleyici uygulayıcıları üzerinde çok fazla yük olacağı için. Örneğin, yapıcı gövdesi başka bir çeviri birimindeyse ne olur? Buna karşılık, dönüş değeri kuralı, returnanahtar sözcükten sonraki tanımlayıcının otomatik bir nesneyi gösterip göstermediğini belirlemek için sembol tablolarını kontrol etmelidir.

Ayrıca parameterby değerini de iletebilirsiniz . Gibi sadece hareket türleri unique_ptriçin, henüz yerleşik bir deyim yok gibi görünüyor. Şahsen, arayüzde daha az dağınıklığa neden olduğu için değere göre geçmeyi tercih ederim.

Özel üye işlevleri

C ++ 98, isteğe bağlı, yani bir yerde ihtiyaç duyulduğunda üç özel üye işlevini dolaylı olarak bildirir: kopya oluşturucu, kopya atama işleci ve yıkıcı.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Rvalue referansları çeşitli versiyonlardan geçti. Sürüm 3.0'dan bu yana, C ++ 11 isteğe bağlı iki ek özel üye işlevi bildirir: taşıma yapıcısı ve taşıma atama işleci. Ne VC10 ne de VC11'in henüz 3.0 sürümüyle uyumlu olmadığını unutmayın, bu yüzden bunları kendiniz uygulamanız gerekecektir.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Bu iki yeni özel üye işlevi, yalnızca özel üye işlevlerinden hiçbiri el ile bildirilmezse örtülü olarak bildirilir. Ayrıca, kendi taşıma kurucunuzu veya taşıma atama işlecinizi bildirirseniz, ne kopya oluşturucu ne de kopya atama işleci dolaylı olarak bildirilmez.

Bu kurallar pratikte ne anlama geliyor?

Yönetilmeyen kaynaklara sahip olmayan bir sınıf yazarsanız, beş özel üye işlevinden herhangi birini kendiniz bildirmeniz gerekmez ve doğru kopya semantiği elde edersiniz ve semantiği ücretsiz olarak taşırsınız. Aksi takdirde, özel üye işlevlerini kendiniz uygulamak zorunda kalacaksınız. Tabii ki, sınıfınız hareket semantiğinden yararlanmıyorsa, özel hareket işlemlerini uygulamaya gerek yoktur.

Kopya atama operatörünün ve taşıma atama operatörünün, bağımsız değişkenini değere göre alarak tek bir birleşik atama operatörüne birleştirilebileceğini unutmayın:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

Bu şekilde, uygulamak için özel üye işlevlerinin sayısı beşten dörde düşer. Burada istisna güvenlik ve verimlilik arasında bir denge vardır, ancak bu konuda uzman değilim.

Referansları yönlendirme ( önceden Universal referansları olarak biliniyordu )

Aşağıdaki işlev şablonunu düşünün:

template<typename T>
void foo(T&&);

T&&Sadece değerlere bağlanmayı bekleyebilirsiniz , çünkü ilk bakışta bir değer referansına benziyor. Yine de ortaya çıktığı gibi, T&&değerlere de bağlanır:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Bağımsız değişken tipte bir rvalue ise X, Tolduğu sonucuna varılmaktadır Xnedenle, T&&anlamına gelir X&&. Herkesin beklediği budur. Argüman türünde bir lvalue Ama eğer Xözel bir kural nedeniyle, Tolması sonucuna varılmaktadır X&dolayısıyla T&&gibi bir şey anlamına gelecektir X& &&. C ++ hala referanslarına referansların bağı yoktur çünkü Ama, tip X& &&olduğu çöktü içine X&. Bu başlangıçta kafa karıştırıcı ve işe yaramaz gelebilir, ancak mükemmel çöküş için referans çökmesi şarttır (burada tartışılmayacaktır).

T&& bir rvalue referansı değil, bir yönlendirme referansıdır. Ayrıca hangi durumda SolDeğerler bağlanır Tve T&&her ikisi lvalue referanslarıdır.

Bir işlev şablonunu değerlerle sınırlamak istiyorsanız, SFINAE'yi tür özellikleriyle birleştirebilirsiniz :

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Hareketin uygulanması

Artık referans daralmasını anladığınıza göre, std::moveşu şekilde uygulanır:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

Gördüğünüz gibi move, yönlendirme referansı sayesinde her türlü parametreyi kabul eder T&&ve bir değer referansı döndürür. std::remove_reference<T>::typeAksi takdirde Çeşidi SolDeğerler için çünkü meta işlev çağrısı gereklidir X, dönüş tipi olacağını X& &&içine çökerdi, hangi X&. Yana tdaima lvalue (a adlandırılmış rvalue referans bir lvalue olduğunu unutmayın), ama biz bağlama istiyoruz tbir rvalue referansına, biz açıkça döküm zorunda tdoğru dönüş tipine. Rvalue referansı döndüren bir işlevin çağrılması, kendisi bir xvalue'dur. Artık x değerlerinin nereden geldiğini biliyorsunuz;)

std::moveRvalue referansı döndüren bir işlevin çağrısı, xvalue'dur.

Bu örnekte, değer referansıyla döndürmenin iyi olduğunu unutmayın, çünkü totomatik bir nesneyi değil, arayan tarafından geçirilen bir nesneyi gösterir.



24
Üçüncü bir neden hareket semantiği önemlidir: istisna güvenliği. Çoğunlukla bir kopyalama işleminin atabileceği yerlerde (kaynak tahsisi gerektiği ve ayırma başarısız olabileceği için) bir taşıma işlemi atılmaz (çünkü yeni kaynakların tahsis edilmesi yerine mevcut kaynakların sahipliğini aktarabilir). Başarısız olan işlemlere sahip olmak her zaman güzeldir ve istisna garantileri sağlayan kod yazarken çok önemli olabilir.
Brangdon

8
'Evrensel referanslara' kadar sizinle birlikteydim, ama o zaman takip etmek için çok soyut. Referans çöküyor mu? Mükemmel yönlendirme? Bir tür referansının, tür ayarlanmışsa evrensel bir referans haline geldiğini mi söylüyorsunuz? Keşke bunu açıklamanın bir yolu olsaydı, anlaması gerekip gerekmediğini bilmek isterdim! :)
Kylotan

8
Lütfen şimdi bir kitap yazın ... Bu cevap bana C ++ 'ın diğer köşelerini böyle berrak bir şekilde kapatırsanız, binlerce insanın bunu anlayacağına inanmam için bir neden verdi.
halivingston

12
@halivingston Geri bildiriminiz için çok teşekkür ederim, gerçekten minnettarım. Kitap yazmanın problemi: hayal edebileceğinizden çok daha fazla iş. C ++ 11 ve ötesini derinlemesine incelemek istiyorsanız, Scott Meyers tarafından "Effective Modern C ++" satın almanızı öneririm.
fredoverflow

77

Taşıma semantiği, rvalue referanslarına dayanır .
Bir değer, ifadenin sonunda yok edilecek geçici bir nesnedir. Geçerli C ++ 'da, değerler yalnızca constreferanslara bağlanır . C ++ 1x, bir rvalue nesnelerine başvurular olan constyazılmamış rvalue dışı başvurulara izin verir T&&.
Bir ifadenin sonunda bir değer öleceğinden , verilerini çalabilirsiniz . Başka bir nesneye kopyalamak yerine , verilerini nesneye taşırsınız .

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

Yukarıdaki kodda, eski derleyiciler ile sonucu f()olduğu kopyalanan içine xkullanarak X'nin kopya kurucu. Derleyiciniz hareket semantiğini destekliyorsa ve Xbir hareket yapıcısı varsa, bunun yerine bu çağrılır. Onun bu yana rhsbağımsız değişken bir olan rvalue , bunun artık gerekli değil biliyoruz ve değerini çalabilir.
Değer Böylece hareket dönen isimsiz geçici gelen f()için x(verilerine ise xboş için başlatılır, X, atama sonra imha alacak olan geçici taşınır).


1
bunun this->swap(std::move(rhs));adı verilen
rvalue

Bu Tacyt yorumuna @ başına, biraz yanlıştır: rhsBir olan lvalue bağlamında X::X(X&& rhs). std::move(rhs)Bir rvalue almak için aramalısın , ama bu biraz cevabı tartışıyor.
Asherah

İşaretçi olmayan türler için anlambilimi ne taşır? Anlambilimi taşımak benzer kopya çalışır?
Gusev Slava

@Gusev: Ne istediğini bilmiyorum.
sbi

60

Önemli bir nesne döndüren bir işleve sahip olduğunuzu varsayın:

Matrix multiply(const Matrix &a, const Matrix &b);

Böyle bir kod yazdığınızda:

Matrix r = multiply(a, b);

sıradan bir C ++ derleyicisi, sonuç için geçici bir nesne multiply()oluşturur, başlatmak için kopya yapıcısını çağırır rve sonra geçici dönüş değerini yok eder. C ++ 0x'deki semantikleri taşıyın "move yapıcısı" nın riçeriğini kopyalayarak başlatılmasını sağlar ve sonra geçici değeri yok etmek zorunda kalmadan atar.

Bu, ( Matrixyukarıdaki örnekte olduğu gibi), kopyalanan nesnenin yığın üzerinde dahili sunumunu saklamak için fazladan bellek ayırması durumunda özellikle önemlidir . Bir kopya oluşturucu, ya iç gösterimin tam bir kopyasını oluşturmak ya da referans sayımı ve yazma üzerine kopya semantiğini birlikte kullanmak zorundadır. Bir hareket yapıcısı yığın belleğini yalnız bırakır ve sadece işaretçiyi Matrixnesnenin içine kopyalar .


2
Taşıma yapıcıları ile kopya yapıcılar arasındaki farklar
dicroce

1
@dicroce: Sözdizimine göre farklılık gösterir, biri Matrix (const Matrix & src) (kopya oluşturucu) ve diğeri Matrix (Matrix && src) (hareket oluşturucu) gibi görünür, daha iyi bir örnek için ana cevabımı kontrol edin.
snk_kid

3
@dicroce: Biri boş bir nesne, diğeri de bir kopya oluşturur. Nesnede depolanan veriler büyükse, bir kopya pahalı olabilir. Örneğin, std :: vector.
Billy ONeal

1
@ kunj2aan: Derleyicinize bağlı, şüpheliyim. Derleyici, işlevin içinde geçici bir nesne oluşturabilir ve sonra onu arayanın dönüş değerine taşıyabilir. Veya nesneyi, bir hareket yapıcısı kullanmadan doğrudan dönüş değerinde oluşturabilir.
Greg Hewgill

2
@Jichao: Bu RVO adlı bir optimizasyon, fark hakkında daha fazla bilgi için bu soruya bakın: stackoverflow.com/questions/5031778/…
Greg Hewgill

30

Eğer hareket semantiğinin iyi ve derinlemesine bir açıklamasıyla ilgileniyorsanız, üzerlerindeki orijinal makaleyi okumanızı tavsiye ederim, "C ++ Diline Hareket Semantiği Desteği Ekleme Önerisi."

Çok erişilebilir ve okunması kolay ve sunduğu avantajlar için mükemmel bir durum. WG21 web sitesinde hareket semantiği hakkında daha yeni ve güncel makaleler var , ancak bu, üst düzey bir bakış açısıyla şeylere yaklaştığı ve cesur dil ayrıntılarına çok fazla girmediği için muhtemelen en basit olanıdır.


27

Semantiği taşıma , artık kaynak değere kimseye ihtiyaç duymadığında kaynakları kopyalamak yerine aktarmakla ilgilidir .

C ++ 03'te, nesneler genellikle kopyalanır, yalnızca herhangi bir kod değeri tekrar kullanmadan önce yok edilir veya atanır. Örneğin, RVO devreye girmedikçe bir işlevden değere göre döndüğünüzde, döndürdüğünüz değer arayanın yığın çerçevesine kopyalanır ve kapsam dışına çıkar ve yok edilir. Bkz pass-by-value kaynak nesne geçici olduğunda, gibi algoritmalar: Bu örneklerden sadece biridir sortbu sadece yeniden düzenlemek öğeleri içinde yeniden tahsis vectoronun zaman capacity()aşıldığında, vb

Bu tür kopyalama / yok etme çiftleri pahalı olduğunda, bunun nedeni genellikle nesnenin ağır bir kaynağa sahip olmasıdır. Örneğin , her biri kendi dinamik belleğine sahip vector<string>bir dizi stringnesne içeren dinamik olarak ayrılmış bir bellek bloğuna sahip olabilir. Böyle bir nesneyi kopyalamak pahalıdır: kaynaktaki dinamik olarak ayrılan her blok için yeni bellek ayırmanız ve tüm değerleri kopyalamanız gerekir. O zaman kopyaladığınız tüm hafızayı yeniden konumlandırmanız gerekir. Bununla birlikte, büyük bir taşımakvector<string> , sadece birkaç noktayı (dinamik bellek bloğuna atıfta bulunur) hedefe kopyalamak ve bunları kaynakta sıfırlamak anlamına gelir.


23

Kolay (pratik) terimlerle:

Bir nesnenin kopyalanması, "statik" üyelerini kopyalamak ve newoperatörü dinamik nesneleri için çağırmak anlamına gelir . Sağ?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Ancak, bir nesneyi taşımak (tekrarlıyorum, pratik bir bakış açısıyla) yenilerini oluşturmak için değil, yalnızca dinamik nesnelerin işaretleyicilerini kopyalamayı gerektirir.

Ama bu tehlikeli değil mi? Tabii ki, dinamik bir nesneyi iki kez yok edebilirsiniz (segmentasyon hatası). Bundan kaçınmak için, kaynak göstergelerini iki kez yok etmemek için "geçersiz kılmalısınız":

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Tamam, ama bir nesneyi hareket ettirirsem, kaynak nesne işe yaramaz hale gelir, değil mi? Tabii ki, ancak bazı durumlarda bu çok yararlı. En belirgin olanı, anonim bir nesneyle (geçici, rvalue nesnesi, ..., farklı adlarla çağırabilirsiniz) bir işlevi çağırdığımdadır:

void heavyFunction(HeavyType());

Bu durumda, anonim bir nesne oluşturulur, daha sonra function parametresine kopyalanır ve daha sonra silinir. Bu nedenle, nesneyi taşımak daha iyidir, çünkü anonim nesneye ihtiyacınız yoktur ve zamandan ve hafızadan tasarruf edebilirsiniz.

Bu bir "değer" referansı kavramına yol açar. C ++ 11'de yalnızca alınan nesnenin anonim olup olmadığını tespit etmek için bulunurlar. Ben zaten bir "lvalue" atanabilir bir varlık ( =operatörün sol kısmı ) olduğunu biliyorum, bu yüzden bir lvalue olarak hareket edebilmek için bir nesneye adlandırılmış bir başvuru gerekir. Bir değer tam tersi, adlandırılmış referansı olmayan bir nesne. Bu nedenle, anonim nesne ve değer eş anlamlılardır. Yani:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

Bu durumda, bir tür nesnenin A"kopyalanması" gerektiğinde, derleyici, iletilen nesnenin adlandırılıp adlandırılmadığına göre bir lvalue referansı veya bir rvalue referansı oluşturur. Değilse, hareket oluşturucunuz çağrılır ve nesnenin zamansal olduğunu bilirsiniz ve dinamik nesnelerini kopyalamak yerine hareket ettirebilirsiniz, yer ve bellek tasarrufu yapabilirsiniz.

"Statik" nesnelerin her zaman kopyalandığını hatırlamak önemlidir. Statik bir nesneyi (yığındaki nesne değil, yığındaki nesne) "taşımanın" yolu yoktur. Dolayısıyla, bir nesnenin (doğrudan veya dolaylı olarak) dinamik bir üyesi olmadığında "taşı" / "kopyala" ayrımı önemsizdir.

Nesneniz karmaşıksa ve yıkıcı bir kütüphanenin işlevini çağırmak, diğer küresel işlevleri çağırmak veya her ne olursa olsun, ikincil efektlere sahipse, belki de bayraklı bir hareketi işaret etmek daha iyidir:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Yani, kodunuz daha kısadır ( nullptrher dinamik üye için bir atama yapmanız gerekmez ) ve daha geneldir.

Diğer tipik soru: A&&ve arasındaki fark const A&&nedir? Tabii ki, ilk durumda, nesneyi değiştirebilir ve ikincisinde pratik anlam değil? İkinci durumda, onu değiştiremezsiniz, bu nedenle nesneyi geçersiz kılmanın hiçbir yolu yoktur (değiştirilebilir bir bayrak veya bunun gibi bir şey hariç) ve bir kopya oluşturucu için pratik bir fark yoktur.

Ve ne mükemmel yönlendirme ? Bir "değer referansı" nın "arayanın kapsamındaki" adlandırılmış bir nesneye başvuru olduğunu bilmek önemlidir. Ancak gerçek kapsamda, bir rvalue referansı bir nesnenin adıdır, bu nedenle adlandırılmış bir nesne gibi davranır. Başka bir işleve bir rvalue başvurusu iletirseniz, adlandırılmış bir nesne iletirsiniz, bu nedenle nesne geçici bir nesne gibi alınmaz.

void some_function(A&& a)
{
   other_function(a);
}

Nesne a, öğesinin gerçek parametresine kopyalanır other_function. Nesnenin ageçici bir nesne olarak işlem görmeye devam etmesini istiyorsanız, şu std::moveişlevi kullanmalısınız :

other_function(std::move(a));

Bu hat sayesinde, std::movedöküm olacak abir rvalue ve other_functionbir adsız bir nesne olarak nesneyi alacak. Tabii ki, other_functionadsız nesnelerle çalışmak için belirli bir aşırı yüklemesi yoksa , bu ayrım önemli değildir.

Bu mükemmel yönlendirme mi? Değil, ama biz çok yakınız. Mükemmel yönlendirme sadece şablonlarla çalışmak için yararlıdır, amacı: bir nesneyi başka bir işleve geçirmem gerekiyorsa, adlandırılmış bir nesne alırsam, nesnenin adlandırılmış bir nesne olarak geçirilmesi gerekir ve Adsız bir nesne gibi iletmek istiyorum:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Bu, C ++ 11'de aracılığıyla uygulanan mükemmel iletimi kullanan prototip bir işlevin imzasıdır std::forward. Bu işlev, şablon örneklemenin bazı kurallarından yararlanır:

 `A& && == A&`
 `A&& && == A&&`

Yani, eğer ( T = A &) Tiçin bir değer referansı ise , ( A & && => A &). Eğer bir rvalue referans , ayrıca, (A && && => bir &&). Her iki durumda da, gerçek kapsamda adlandırılmış bir nesnedir, ancak arayan kapsamının bakış açısından "referans türü" bilgisini içerir. Bu bilgi ( ), şablon parametresi olarak iletilir ve 'a', türüne göre taşınır veya taşınır .AaTAaaTTforwardT


20

Bu, kopya semantiği gibidir, ancak "taşınan" nesneden verileri çalmak için elde ettiğiniz tüm verileri çoğaltmak yerine.


13

Bir kopya semantiğinin ne anlama geldiğini biliyor musunuz? Bu, kopyalanabilen türleriniz olduğu anlamına gelir; bunu tanımladığınız kullanıcı tanımlı türler için, açıkça bir kopya oluşturucu ve atama işleci yazmak veya derleyici bunları dolaylı olarak üretir. Bu bir kopyasını yapar.

Hareket semantiği temel olarak r-değeri referansını (&& (evet iki ve işareti kullanarak yeni referans türü) alan yapıcıya sahip, sabit olmayan bir kullanıcı tanımlı türdür, buna hareket oluşturucu denir, aynı atama operatörü için de geçerlidir. Bu nedenle, bir hareket yapıcısı ne yapar, hafızayı kaynak argümanından kopyalamak yerine hafızayı kaynaktan hedefe 'taşır'.

Ne zaman yapmak istersin? well std :: vector bir örnektir, geçici bir std :: vector oluşturduğunuzu ve bir fonksiyondan geri döndürdüğünüzü söyleyin:

std::vector<foo> get_foos();

İşlev döndüğünde, kopya oluşturucudan ek yüke sahip olacaksınız (ve C ++ 0x'de olacaksa) std :: vector kopyalamak yerine bir hareket yapıcısına sahipse, yalnızca işaretçileri ayarlayabilir ve dinamik olarak ayrılmış 'taşı' yeni örneğe bellek. Std :: auto_ptr ile sahiplik aktarımı semantiği gibi.


1
Ben bu harika bir örnek olduğunu sanmıyorum, çünkü bu işlev dönüş değeri örneklerinde Dönüş Değeri Optimizasyonu muhtemelen kopyalama işlemini zaten ortadan kaldırıyor.
Zan Lynx

7

Hareket semantiği ihtiyacını göstermek için, hareket semantiği olmadan bu örneği ele alalım:

İşte bir tür Tnesneyi alan ve aynı türden bir nesne döndüren bir işlev T:

T f(T o) { return o; }
  //^^^ new object constructed

Yukarıdaki işlev, değere göre çağrı kullanır ; bu, bu işlev çağrıldığında işlev tarafından kullanılmak üzere bir nesne yapılandırılması gerektiği anlamına gelir .
İşlev de değere göre döndüğünden , dönüş değeri için başka bir yeni nesne oluşturulur:

T b = f(a);
  //^ new object constructed

İki yeni nesne inşa edildi, bunlardan biri sadece işlev süresi boyunca kullanılan geçici bir nesne.

Yeni nesne dönüş değerinden oluşturulduğunda, kopya yapıcısı çağrılır kopyalamak yeni nesne b geçici nesnenin içeriği. İşlev tamamlandıktan sonra, işlevde kullanılan geçici nesne kapsam dışına çıkar ve yok edilir.


Şimdi, bir kopya oluşturucunun ne yaptığını düşünelim .

Önce nesneyi başlatmalı, ardından eski nesneden tüm ilgili verileri yenisine kopyalamalıdır.
Sınıfa bağlı olarak, belki de çok fazla veri içeren bir kap, o zaman çok fazla zaman ve bellek kullanımını temsil edebilir

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Hareket semantiği ile artık verileri kopyalamak yerine hareket ettirerek bu işin çoğunu daha az tatsız hale getirmek mümkün .

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Verilerin taşınması, verilerin yeni nesne ile yeniden ilişkilendirilmesini içerir. Ve hiçbir kopya yer almıyor .

Bu bir rvaluereferansla gerçekleştirilir .
Bir rvaluereferans hemen hemen bir gibi çalışır lvalueönemli bir farkla referans:
Bir rvalue referans hareket ettirilebilir ve bir lvalue olamaz.

Gönderen cppreference.com :

Güçlü istisna garantisini mümkün kılmak için, kullanıcı tanımlı hareket kurucuları istisnalar atmamalıdır. Aslında, standart kaplar, kap öğelerinin yerlerinin değiştirilmesi gerektiğinde taşıma ve kopyalama arasında seçim yapmak için genellikle std :: move_if_noexcept yöntemini kullanır. Hem kopyalama hem de taşıma yapıcıları sağlandıysa, aşırı yük çözünürlüğü, bağımsız değişken bir değerse (adsız geçici gibi bir değer veya std :: move sonucu gibi bir xvalue) ise taşıma yapıcısını seçer ve argüman bir lvalue (adlandırılmış nesne veya lvalue başvurusu döndüren bir işlev / işleç). Yalnızca kopya yapıcısı sağlanmışsa, tüm bağımsız değişken kategorileri onu seçer (const'a bir başvuru gerektirdiği sürece, değerler const referanslarına bağlanabilir), bu da hareket etmek için geri dönüşün kopyalanmasını mümkün olmadığında yapar. Birçok durumda, hareket oluşturucuları gözlemlenebilir yan etkiler üretseler bile optimize edilirler, bkz. Kopya seçimi. Bir kurucu, parametre olarak bir rvalue referansı aldığında 'move yapıcısı' olarak adlandırılır. Hiçbir şeyi taşımak zorunda değildir, sınıfın taşınacak bir kaynağa sahip olması gerekmez ve bir 'taşıma yapıcısı' bir parametrenin izin verilen (ancak belki de mantıklı olmayan) durumda bir kaynağı taşıyamayabilir. sabit değer referansı (cons & T &&).


7

Bunu doğru anladığımdan emin olmak için yazıyorum.

Büyük nesnelerin gereksiz kopyalanmasını önlemek için taşıma semantiği oluşturuldu. "C ++ Programlama Dili" kitabında Bjarne Stroustrup, varsayılan olarak gereksiz kopyalamanın meydana geldiği iki örnek kullanır: bir, iki büyük nesnenin değiştirilmesi ve iki, büyük bir nesnenin bir yöntemden döndürülmesi.

İki büyük nesnenin değiştirilmesi genellikle ilk nesneyi geçici bir nesneye, ikinci nesneyi ilk nesneye kopyalamayı ve geçici nesneyi ikinci nesneye kopyalamayı içerir. Yerleşik bir tür için bu çok hızlıdır, ancak büyük nesneler için bu üç kopya çok zaman alabilir. Bir "taşıma ataması" programlayıcının varsayılan kopya davranışını geçersiz kılmasına ve bunun yerine nesnelere yapılan başvuruları değiştirmesine olanak tanır, bu da hiçbir kopyalamanın olmadığı ve takas işleminin çok daha hızlı olduğu anlamına gelir. Taşıma ataması std :: move () yöntemi çağrılarak çağrılabilir.

Bir nesneyi bir yöntemden varsayılan olarak döndürmek, yerel nesnenin ve onunla ilişkili verilerin, arayan tarafından erişilebilen bir konumda kopyalanmasını içerir (çünkü yerel nesneye arayan tarafından erişilemez ve yöntem bittiğinde kaybolur). Yerleşik bir tür döndürüldüğünde, bu işlem çok hızlıdır, ancak büyük bir nesne döndürülüyorsa, bu işlem uzun sürebilir. Taşıma yapıcısı programcının bu varsayılan davranışı geçersiz kılmasına ve bunun yerine yerel nesneyle ilişkili verileri yığınlamak için çağırana döndürülen nesneyi işaret ederek yerel nesne ile ilişkili yığın verilerini "yeniden" kullanmasına izin verir. Bu nedenle kopyalamaya gerek yoktur.

Yerel nesnelerin (yani yığın üzerindeki nesnelerin) oluşturulmasına izin vermeyen dillerde, bu tür sorunlar, tüm nesneler öbek üzerinde tahsis edildiğinden ve her zaman referans olarak erişildiğinden oluşmaz.


"A" taşıma ataması "programlayıcının varsayılan kopyalama davranışını geçersiz kılmasına ve bunun yerine nesnelere yapılan başvuruları değiştirmesine olanak tanır, bu da hiçbir kopyalama olmadığı ve takas işleminin çok daha hızlı olduğu anlamına gelir." - bu iddialar belirsiz ve yanıltıcıdır. İki nesneyi takas etmek xve yşunları yapabilirsiniz sadece "nesnelere takas referanslar" ; nesnelerde başka verilere başvuran işaretçiler bulunabilir ve bu işaretçiler değiştirilebilir, ancak hareket işleçlerinin hiçbir şeyi değiştirmeleri gerekmez . Buradaki hedef verileri korumak yerine verileri taşınan nesneden silebilirler.
Tony Delroy

swap()Hareket semantiği olmadan yazabilirsiniz . "Taşıma ataması std :: move () yöntemi çağrılarak çağrılabilir." - bazen kullanmak gerekir std::move()- bu aslında hiçbir şeyi hareket ettirmez - derleyiciye argümanın hareketli olduğunu, bazen std::forward<>()(yönlendirme referanslarıyla) ve diğer zamanlarda derleyicinin bir değerin taşınabileceğini bilmesini sağlar.
Tony Delroy

-2

İşte Bjarne Stroustrup'un "C ++ Programlama Dili" kitabından bir cevap . Videoyu görmek istemiyorsanız, aşağıdaki metni görebilirsiniz:

Bu pasajı düşünün. Bir işleçten + dönmek, sonucun yerel değişkenden resve arayanın erişebileceği bir yere kopyalanmasını içerir .

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Gerçekten bir kopya istemedik; sonucu bir fonksiyondan çıkarmak istedik. Bu yüzden bir Vector öğesini kopyalamak yerine taşımak zorundayız. Move yapıcısını şu şekilde tanımlayabiliriz:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&&, "rvalue referansı" anlamına gelir ve bir rvalue'yu bağlayabileceğimiz bir referanstır. "değer", kabaca "ödevin sol tarafında görünebilecek bir şey" anlamına gelen "değer" değerini tamamlamayı amaçlamaktadır. Dolayısıyla bir değer kabaca bir işlev çağrısı tarafından döndürülen bir tamsayı ve resVektörler için + () operatörünün yerel değişkeni gibi kabaca "atayamayacağınız bir değer" anlamına gelir .

Şimdi ifade return res;kopyalanmayacak!

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.