C ++ 11 rvalues ​​ve move semantik karışıklığı (return ifadesi)


435

Rvalue referanslarını anlamaya ve C ++ 11 semantiği taşımak çalışıyorum.

Bu örnekler arasındaki fark nedir ve hangileri vektör kopyası yapmayacaktır?

İlk örnek

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

İkinci örnek

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Üçüncü örnek

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

51
Lütfen yerel değişkenleri referans olarak döndürmeyin. Bir rvalue referansı hala bir referanstır.
fredoverflow

63
Örnekler arasındaki anlamsal farklılıkları anlamak için açıkça
kasıtlıydı

@FredOverflow Eski soru, ancak yorumunuzu anlamak bir saniyemi aldı. Sanırım # 2 ile ilgili soru, std::move()kalıcı bir "kopya" oluşturulup oluşturulmadığıydı.
3Dave

5
@DavidLively std::move(expression)hiçbir şey yaratmaz, sadece ifadeyi bir xvalue değerine çevirir. Değerlendirme sürecinde hiçbir nesne kopyalanmaz veya taşınmaz std::move(expression).
fredoverflow

Yanıtlar:


563

İlk örnek

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

İlk örnek yakalanan bir geçici döndürür rval_ref. Bu geçici yaşamını rval_reftanımın ötesine taşıyacak ve onu değere yakalamış gibi kullanabilirsiniz. Bu aşağıdakine çok benzer:

const std::vector<int>& rval_ref = return_vector();

ancak yeniden yazımda açık bir şekilde kullanamazsınız rval_ref.

İkinci örnek

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

İkinci örnekte bir çalışma zamanı hatası oluşturdunuz. rval_refşimdi tmpfonksiyonun içindeki yıkılmış bir referansa sahiptir . Herhangi bir şansla, bu kod hemen çökecektir.

Üçüncü örnek

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Üçüncü örneğiniz kabaca ilk örneğinizle eşdeğerdir. std::moveÜzerinde tmpgereksizdir ve aslında dönüş değeri optimizasyonu inhibe edecek gibi bir performans pessimization olabilir.

Yaptıklarınızı kodlamanın en iyi yolu:

En iyi pratik

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Yani C ++ 03'deki gibi. tmpdolaylı olarak iade ifadesinde bir değer olarak değerlendirilir. Ya dönüş değeri optimizasyonu (kopya yok, hareket yok) ile döndürülecek ya da derleyici RVO yapamayacağına karar verirse , dönüşü yapmak için vektörün hareket yapıcısını kullanacaktır . Yalnızca RVO gerçekleştirilmezse ve döndürülen türde bir hareket yapıcısı yoksa, kopya yapıcısı dönüş için kullanılır.


65
Yerel bir nesneyi değere göre döndürdüğünüzde derleyiciler RVO yapar ve yerelin türü ile işlevin dönüşü aynıdır ve ikisi de cv nitelendirilmez (const türlerini döndürmeyin). RVO'yu engelleyebileceğinden condition (:?) İfadesiyle geri dönmekten kaçının. Yerel öğeyi, yerel öğeye başvuru döndüren başka bir işleve sarmayın. Sadece return my_local;. Birden fazla dönüş ifadesi uygundur ve RVO'yu engellemez.
Howard Hinnant

27
Bir uyarı var: yerel bir nesnenin bir üyesini döndürürken , hareket açık olmalıdır.
boycy

5
@NoSenseEtAl: Dönüş satırında geçici olarak oluşturulmuş bir şey yok. movegeçici oluşturmaz. Bir xvalue değerinde bir lvalue yayınlar, kopya oluşturmaz, hiçbir şey yaratmaz, hiçbir şeyi yok etmez. Bu örnek, lvalue-başvurusu ile döndürdüğünüz moveve dönüş çizgisinden kaldırdığınız durumla tamamen aynıdır : Her iki durumda da, işlev içindeki yerel bir değişkene sarkan bir başvurunuz olur ve bunlar yok edilir.
Howard Hinnant

15
"Çoklu dönüş ifadeleri tamam ve RVO'yu engellemeyecek": Yalnızca aynı değişkeni döndürürlerse .
Deduplicator

5
@Deduplicator: Haklısın. İstediğim kadar doğru konuşmuyordum. Birden fazla dönüş ifadesinin derleyiciyi RVO'dan yasaklamasını (uygulamayı imkansız hale getirmesine rağmen) ve bu nedenle dönüş ifadesinin hala bir değer olarak kabul edildiğini kastetmiştim.
Howard Hinnant

42

Hiçbiri kopyalanmayacak, ancak ikincisi yok edilen bir vektöre atıfta bulunacaktır. Adlandırılmış değer referansları neredeyse hiçbir zaman normal kodda bulunmaz. Sadece bir kopyasını C ++ 03'e nasıl yazacağınızı yazıyorsunuz.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Şimdi hariç, vektör taşınır. Kullanıcı bir sınıfın vakaların büyük çoğunluğunda 's rvalue referanslarla uğraşmaz.


Üçüncü örneğin vektör kopyasını yapacağından gerçekten emin misiniz?
Tarantula

@Tarantula: Vektörünü yakacak. Kırılmadan önce kopyalayıp kopyalamamış olması önemli değildir.
Köpek yavrusu

4
Teklif ettiğin avlanma için hiçbir neden görmüyorum. Yerel bir değer referansı değişkenini bir değere bağlamak son derece iyidir. Bu durumda, geçici nesnenin ömrü, rvalue referans değişkeninin ömrünü uzatır.
fredoverflow

1
Sadece bir açıklama noktası, çünkü bunu öğreniyorum. Bu yeni örnekte, vektör tmpdeğildir taşındı içine rval_ref, ancak doğrudan yazılır rval_ref(yani elision kopya) RVO kullanarak. std::moveKopya seçimleri ile kopya seçimleri arasında bir ayrım vardır . A std::moveyine de kopyalanacak bazı verileri içerebilir; bir vektör durumunda, kopya oluşturucuda aslında yeni bir vektör oluşturulur ve veriler tahsis edilir, ancak veri dizisinin büyük kısmı yalnızca işaretçiyi kopyalayarak kopyalanır (esasen). Kopya seçimi, tüm kopyaların% 100'ünden kaçınır.
Mark Lakata

@MarkLakata Burası NRVO, RVO değil. NRVO, C ++ 17'de bile isteğe bağlıdır. Uygulanmazsa, hem return değeri hem de rval_refdeğişkenler, move yapıcısı kullanılarak oluşturulur std::vector. Hem birlikte olan / olmayan hiçbir kopya oluşturucu yoktur std::move. tmpBir olarak kabul edilir rvalue içinde returnbu durumda açıklamada.
Daniel Langr

16

Bunun basit cevabı, normal referans kodu gibi rvalue referansları için kod yazmanız ve onlara zihinsel olarak% 99 oranında davranmanızdır. Bu, başvuruları döndürmeyle ilgili tüm eski kuralları içerir (yani hiçbir zaman yerel değişkene başvuru döndürmez).

Std :: forward avantajından faydalanmak ve lvalue veya rvalue referanslarını alan genel bir işlev yazmak için bir template container sınıfı yazmıyorsanız, bu aşağı yukarı doğrudur.

Taşıma yapıcısının ve taşıma atamasının en büyük avantajlarından biri, bunları tanımlarsanız, derleyicinin RVO (dönüş değeri optimizasyonu) ve NRVO (adı verilen dönüş değeri optimizasyonu) çağrılamadığı durumlarda bunları kullanabilmesidir. Bu, kapsayıcılar ve dizeler gibi pahalı nesneleri yöntemlerden verimli bir şekilde değerle döndürmek için oldukça büyüktür.

Şimdi, değer referansları ile işlerin ilginçleştiği yer, bunları normal işlevlere argüman olarak da kullanabilmenizdir. Bu, hem const referansı (const foo & diğer) hem de rvalue referansı (foo && other) için aşırı yükleri olan kaplar yazmanıza olanak tanır. Argüman sadece bir yapıcı çağrısı ile geçilemeyecek kadar bile olsa yine de yapılabilir:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

STL kapları, neredeyse her şey (karma anahtarı ve değerleri, vektör ekleme, vb.) İçin aşırı yük taşıyacak şekilde güncellendi ve bunları en çok göreceğiniz yer.

Bunları normal işlevler için de kullanabilirsiniz ve yalnızca bir değer referansı bağımsız değişkeni sağlarsanız, arayanı nesneyi oluşturmaya zorlayabilir ve işlevin hareketi yapmasına izin verebilirsiniz. Bu gerçekten iyi bir kullanımdan daha fazla bir örnektir, ancak oluşturma kütüphanemde, yüklenen tüm kaynaklara bir dize atadım, böylece her nesnenin hata ayıklayıcıda neyi temsil ettiğini görmek daha kolay. Arayüz böyle bir şeydir:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

Bu bir 'sızdıran soyutlama' biçimidir, ancak çoğu zaman dizeyi oluşturmak zorunda olduğum gerçeğinden yararlanmamı ve bunun başka bir kopyasını yapmaktan kaçınmamı sağlar. Bu tam olarak yüksek performanslı bir kod değildir, ancak insanlar bu özelliğin tadını çıkarırken olasılıklara iyi bir örnektir. Bu kod aslında değişkenin çağrı için geçici olmasını veya std :: move invoked olmasını gerektirir:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

veya

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

veya

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

ama bu derlenmeyecek!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

3

Kendi başına bir cevap değil , bir kılavuz. Çoğunlukla yerel T&&değişkeni bildirmenin pek bir anlamı yoktur (yaptığınız gibi std::vector<int>&& rval_ref). Yine de yazım yöntemlerinde std::move()kullanmanız gerekecek foo(T&&). Ayrıca, böyle bir şeyi iade etmeye çalıştığınızda,rval_ref işlevden standart referans-yok edilen-geçici-fiyaskoya sahip olacağınızdan .

Çoğu zaman aşağıdaki desenle giderdim:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

Döndürülen geçici nesneler için herhangi bir referansınız yoktur, bu nedenle taşınan bir nesneyi kullanmak isteyen programcının hatalarından (deneyimsiz) kaçınırsınız.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

Açıkçası (çok nadir olmasına rağmen) bir fonksiyonun gerçekten döndürdüğü durumlar vardır; T&&bu, nesnenize taşınabileceğiniz geçici olmayan bir nesneye referanstır .

RVO ile ilgili olarak: bu mekanizmalar genellikle çalışır ve derleyici kopyalamayı güzel bir şekilde önleyebilir, ancak dönüş yolunun belirgin olmadığı durumlarda (istisnalar, ifadlandırılacak nesneyi belirleyen şartlar ve muhtemelen diğer çiftler) rrefs kurtarıcınızdır (potansiyel olarak daha fazla olsa bile) pahalı).


2

Bunların hiçbiri ekstra kopya yapmayacaktır. RVO kullanılmasa bile, yeni standart, inanıyorum dönüşleri yaparken hareket inşaatının kopyalamayı tercih ettiğini söylüyor.

Yerel bir değişkene bir başvuru döndürdüğünüz için ikinci örneğinizin tanımlanmamış davranışa neden olduğuna inanıyorum.


1

İlk cevaba yapılan yorumlarda daha önce belirtildiği gibi, return std::move(...);yapı yerel değişkenlerin geri dönüşü dışındaki durumlarda fark yaratabilir. Aşağıda, bir üye nesneyi içeren ve içermediğinizde neler olduğunu belgeleyen çalıştırılabilir bir örnek verilmiştir std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

Muhtemelen, return std::move(some_member);sadece belirli bir sınıf üyesini hareket ettirmek istiyorsanız mantıklıdır, örneğin class Ckısa süreli adaptör nesnelerini yalnızcastruct A .

Nasıl Bildirimi struct Aalır hep kopyalanan dışarı class Bbile, class Bnesne bir R-değerdir. Derleyici söylemek hiçbir şekilde sahip olmasıdır class B's örneği struct Aartık kullanılmayacaktır. Olarak class C, derleyici bu bilgileri var std::move()neden olan, struct Aalır taşındı örneği sürece, class Csabittir.

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.