C ++ 11'de T&& (çift ve işareti) ne anlama geliyor?


799

C ++ 11'in yeni özelliklerinden bazılarına baktım ve fark ettiğim, değişkenleri bildirmede çift ve işareti T&& var.

Başlangıç ​​olarak, bu canavara ne denir? Keşke Google bunun gibi noktalama işaretlerini aramamıza izin verseydi.

Tam olarak ne anlama geliyor?

İlk bakışta, çift referans gibi görünüyor (C tarzı çift işaretçiler gibi T** var), ancak bunun için bir kullanım durumu düşünmekte zorlanıyorum.


55
Gelecekte daha fazla geleceğinden eminim çünkü bunu c ++ - faq'a ekledim.
GManNickG


41
Google'ı kullanarak bunu arayabilirsiniz, yalnızca kelime öbeğinizi tırnak içine almanız gerekir: google.com/#q="T%26%26 "artık sorunuzu ilk isabet olarak görüyor. :)
sbi

Burada benzer bir soruya çok iyi, anlaşılması kolay bir yanıt var stackoverflow.com/questions/7153991/…
Daniel

2
Google'da "c ++ iki ve işareti parametresi" için arama yapan en üstte üç yığın akışı sorusu aldım ve ilk soru sizinkiydi. Dolayısıyla, "iki ve işareti parametresini" heceleyebilirseniz bunun için noktalama işaretlerini kullanmanıza bile gerek yoktur.
sergiol

Yanıtlar:


668

Bir rvalue referansı beyan eder (standart teklif dokümanı).

İşte rvalue referanslarına bir giriş .

İşte Microsoft'un standart kitaplık geliştiricilerinden birinin değer referanslarına muhteşem bir bakış .

DİKKAT: MSDN'deki bağlantılı makale ("Rvalue Referansları: VC10, Bölüm 2'deki C ++ 0x Özellikleri) Rvalue referanslarına çok açık bir giriştir, ancak bir zamanlar taslak C ++ 11'de geçerli olan Rvalue referansları hakkında açıklamalar yapar standart, ancak sonuncusu için doğru değil! Özellikle, çeşitli noktalarda değer referanslarının bir zamanlar doğru olan ancak değiştirilen değerlere bağlanabileceğini söylüyor (örneğin int x; int &&rrx = x; artık GCC'de derlenmemektedir) - 13 Temmuz 14: 14

Bir C ++ 03 referansı (şimdi C ++ 11'de lvalue referansı olarak adlandırılır) arasındaki en büyük fark, const olmak zorunda kalmadan geçici gibi bir değere bağlanabilmesidir. Dolayısıyla, bu sözdizimi artık yasaldır:

T&& r = T();

rvalue referansları öncelikle aşağıdakileri sağlar:

Anlambilimi taşı . Artık normal sabit değer referansı yerine bir değer referansı alan bir hareket yapıcı ve hareket atama operatörü tanımlanabilir. Bir taşıma, kaynağı değiştirmeden zorunlu kılmak dışında bir kopya gibi işlev görür; aslında, genellikle taşınan kaynaklara sahip olmayacak şekilde kaynağı değiştirir. Bu, özellikle standart kütüphane uygulamalarında yabancı kopyaları ortadan kaldırmak için mükemmeldir.

Örneğin, bir kopya oluşturucu şöyle görünebilir:

foo(foo const& other)
{
    this->length = other.length;
    this->ptr = new int[other.length];
    copy(other.ptr, other.ptr + other.length, this->ptr);
}

Bu kurucu geçici olarak geçtiyse, kopya gereksiz olacaktır çünkü geçici olanın sadece yok edileceğini biliyoruz; neden geçici olarak tahsis edilen kaynakları kullanmıyorsunuz? C ++ 03'te, geçici geçildiğimizi belirleyemediğimiz için kopyalamayı önlemenin bir yolu yoktur. C ++ 11'de, bir hareket yapıcısını aşırı yükleyebiliriz:

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

Buradaki büyük farka dikkat edin: hareket yapıcı aslında argümanını değiştirir. Bu, geçici olanı inşa edilen nesneye etkili bir şekilde "taşıyacak" ve böylece gereksiz kopyayı ortadan kaldıracaktır.

Taşıma yapıcısı, std::moveişlevler kullanılarak açıkça değer değer referanslarına dönüştürülen geçici dosyalar ve sabit olmayan değer referansları için kullanılır (yalnızca dönüşümü gerçekleştirir). Aşağıdaki kod her ikisi için hareket yapıcı çağırmak f1ve f2:

foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"

Mükemmel yönlendirme . rvalue referansları, şablonlanmış işlevler için bağımsız değişkenleri doğru şekilde iletmemizi sağlar. Örneğin bu fabrika işlevini ele alalım:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
    return std::unique_ptr<T>(new T(a1));
}

Eğer factory<foo>(5)çağırırsak, argüman 'yapıcısı int&bir anahtar kelimeyi ele geçirse bile, değişmez bir 5'e bağlanmaz . Bunun yerine kullanabiliriz , ancak yapıcı argümanını const olmayan referansla alırsa ne olur ? Gerçekten jenerik fabrika işlevini hale getirmek için, üzerinde fabrika aşırı zorunda kalacak ve üzerinde . Fabrika 1 parametre türünü alırsa bu iyi olabilir, ancak her ek parametre türü 2 tarafından ayarlanan gerekli aşırı yükü çarpar. Bu çok hızlı bir şekilde sürdürülemez.foointA1 const&fooA1&A1 const&

rvalue referansları, standart kitaplığın std::forwardlvalue / rvalue başvurularını düzgün şekilde iletebilen bir işlev tanımlamasına izin vererek bu sorunu giderir . Nasıl std::forwardçalıştığı hakkında daha fazla bilgi için bu mükemmel yanıta bakın .

Bu, fabrika fonksiyonunu şu şekilde tanımlamamızı sağlar:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
    return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}

Şimdi argümanın rvalue / lvalue-ness değeri, Tyapıcısına aktarıldığında korunur . Bu, eğer fabrika bir değerle çağrılırsa, Tyapıcısına bir değerle çağrılır. Eğer fabrika bir lvalue ile çağrılırsa, Tyapıcı bir lvalue ile çağrılır. Geliştirilmiş fabrika işlevi, özel bir kural nedeniyle çalışır:

İşlev parametresi türü, şablon parametresinin T&&bulunduğu formda Tolduğunda ve işlev bağımsız değişkeni bir tür değeri Aolduğunda, tür A&, şablon bağımsız değişkeninin kesilmesi için kullanılır.

Böylece, biz fabrika kullanabilirsiniz gibi:

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)

Önemli rvalue referans özellikleri :

  • Aşırı yük çözünürlüğü için, değerler değerler değer referanslarına bağlanmayı ve değerler değerler değer referanslarına bağlanmayı tercih eder . Bu nedenle geçiciler neden bir kopya oluşturucu / atama işleci yerine bir taşıma yapıcısı / taşıma atama işleci çağırmayı tercih eder.
  • rvalue referansları, örtük bir dönüştürme işleminin sonucu olan rvalue'lara ve geçici programlara dolaylı olarak bağlanır . yani float f = 0f; int&& i = f;iyi biçimlendirilmiştir, çünkü şamandıra dolaylı olarak int'e dönüştürülebilir; referans, dönüşümün sonucu olan bir geçici olacaktır.
  • Adlandırılmış değer referansları değerlerdir. Adsız değer referansları değerlerdir. Bu, std::moveçağrının neden gerekli olduğunu anlamak için önemlidir :foo&& r = foo(); foo f = std::move(r);

65
İçin +1 Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; Bunu bilmeden insanların neden T &&t; std::move(t);uzun süre hareket eden kraterlerde ve benzerlerinde bir şeyler yaptığını anlamaya çalıştım .
legends2k

@MaximYegorushkin: Bu örnekte, r saf bir değere (geçici) bağlanıyor ve bu nedenle geçici, ömrünü uzatmalı, değil mi?
Peter Huene

@PeterHuene Bunu geri alıyorum, r-değeri referansı geçici bir ömrünü uzatır.
Maxim Egorushkin

32
DİKKAT : MSDN ( "rvalue Referanslar: C ++ 0x VC10, Bölüm 2 Özellikleri") üzerine bağlantılı makale olduğunu rvalue referanslarına çok net giriş, ama vardı rvalue başvurular hakkında açıklamalarda yapar kez taslak C ++ 11 gerçek standart, ancak sonuncusu için doğru değil ! Özellikle, çeşitli noktalarda değer referanslarının bir zamanlar doğru olan ancak değiştirilen değerlere bağlanabileceğini söylüyor (örneğin int x; int &&rrx = x; artık GCC'de derlenmiyor )
drewbarbs

@PeterHuene Yukarıdaki örnekte, typename identity<T>::type& aeşdeğer değil T&mi?
ibp73

81

Bir rvalue referansını gösterir. Rvalue referansları, aksi açıkça belirtilmedikçe yalnızca geçici nesnelere bağlanır. Belirli koşullar altında nesneleri çok daha verimli hale getirmek ve mükemmel yönlendirme olarak bilinen ve şablon kodunu büyük ölçüde basitleştiren bir tesis sağlamak için kullanılırlar.

C ++ 03'te, değiştirilemez bir değer ve kopya değeri arasında bir ayrım yapamazsınız.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);

C ++ 0x, durum böyle değil.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);

Bu kurucuların arkasındaki uygulamayı düşünün. İlk durumda, dize değer semantiğini korumak için yeni bir yığın tahsisi içeren bir kopya gerçekleştirmelidir. Bununla birlikte, ikinci durumda, kurucumuza aktarılan nesnenin derhal yıkımdan kaynaklandığını ve dokunulmadan kalması gerektiğini önceden biliyoruz. Sadece dahili göstergeleri değiştirebilir ve bu senaryoda hiçbir şekilde kopyalama yapamayız ki bu da çok daha verimlidir. Hareket semantiği, dahili olarak başvurulan kaynakların pahalı veya yasaklanmış kopyalarına sahip herhangi bir sınıfa yarar sağlar. Şunu düşünün std::unique_ptr- şimdi sınıfımız geçici ve geçici olmayanları birbirinden ayırabildiğinden, hareket semantiğinin doğru çalışmasını sağlayabiliriz, böylece unique_ptrkopyalanamaz ancak hareket ettirilebilir, yanistd::unique_ptryasal olarak Standart kaplarda saklanabilir, sıralanabilir, vb. C ++ 03'ler ise saklanamaz std::auto_ptr.

Şimdi, rvalue referanslarının diğer kullanımını - mükemmel yönlendirme olarak ele alıyoruz. Bir referansı referansa bağlama konusunu düşünün.

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template

C ++ 03'ün bu konuda ne söylediğini hatırlayamıyorum, ancak C ++ 0x'de, sonuç referanslarıyla uğraşırken ortaya çıkan tür kritiktir. T'nin bir referans tipi olduğu T tipine bir rvalue referansı, T tipinin bir referansı haline gelir.

(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&

En basit şablon fonksiyonunu düşünün - min ve maks. C ++ 03'te const ve const olmayan dört kombinasyonun tümü için aşırı yükleme yapmanız gerekir. C ++ 0x sadece bir aşırı yük. Variadic şablonlarla birleştirildiğinde, mükemmel yönlendirme sağlar.

template<typename A, typename B> auto min(A&& aref, B&& bref) {
    // for example, if you pass a const std::string& as first argument,
    // then A becomes const std::string& and by extension, aref becomes
    // const std::string&, completely maintaining it's type information.
    if (std::forward<A>(aref) < std::forward<B>(bref))
        return std::forward<A>(aref);
    else
        return std::forward<B>(bref);
}

Dönüş tipi kesinti bıraktım, çünkü nasıl hazırlıksız yapıldığını hatırlayamıyorum, ama bu min değerlerin, değerlerin, sabit değerlerin herhangi bir kombinasyonunu kabul edebilir.


Neden kullanılan std::forward<A>(aref) < std::forward<B>(bref)? ve ileriye baktığınızda bu tanımın doğru olacağını düşünmüyorum int&ve float&. Daha iyi bir tür form şablonu bırakın.
Yankes

25

Terim T&& türü kesinti ile birlikte kullanıldığında (örneğin, mükemmel bir yönlendirme şekilde) olarak halk dilinde bilinen yönlendirme referans . "Evrensel referans" terimi bu makalede Scott Meyers tarafından icat edildi , ancak daha sonra değiştirildi.

Çünkü r-değeri veya l-değeri olabilir.

Örnekler:

// template
template<class T> foo(T&& t) { ... }

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;

Daha fazla tartışma yanıtında bulunabilir: Evrensel referanslar için sözdizimi


14

Rvalue referansı, birkaç istisna dışında, sıradan X & referansına çok benzeyen bir tiptir. En önemlisi, aşırı yük çözünürlüğü söz konusu olduğunda, değerlerin eski tip değer referanslarını tercih ederken, değerlerin yeni değer referanslarını tercih etmesidir:

void foo(X& x);  // lvalue reference overload
void foo(X&& x); // rvalue reference overload

X x;
X foobar();

foo(x);        // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)

Peki bir değer nedir? Lvalue olmayan her şey. Lvalue, bir bellek konumuna işaret eden ve bu bellek konumunun adresini & işleci aracılığıyla almamızı sağlayan bir ifadedir.

Bir örnekle hangi değerlerin başardığını anlamak neredeyse daha kolaydır:

 #include <cstring>
 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {
     if (ptr != nullptr) memset(ptr, 0, sz);
  }
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     if (ptr != nullptr) memcpy(ptr, s.ptr, s.size);
     std::cout << "copy constructor called on lvalue\n";
  }

  // move constructor that take rvalue
  Sample(Sample&& s) 
  {  // steal s's resources
     ptr = s.ptr;
     size = s.size;        
     s.ptr = nullptr; // destructive write
     s.size = 0;
     cout << "Move constructor called on rvalue." << std::endl;
  }    
  // normal copy assignment operator taking lvalue
  Sample& operator=(const Sample& s)
  {
   if(this != &s) {
      delete [] ptr; // free current pointer
      size = s.size;

      if (size != 0) {
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
      } else 
         ptr = nullptr;
     }
     cout << "Copy Assignment called on lvalue." << std::endl;
     return *this;
  }    
 // overloaded move assignment operator taking rvalue
 Sample& operator=(Sample&& lhs)
 {
   if(this != &s) {
      delete [] ptr; //don't let ptr be orphaned 
      ptr = lhs.ptr;   //but now "steal" lhs, don't clone it.
      size = lhs.size; 
      lhs.ptr = nullptr; // lhs's new "stolen" state
      lhs.size = 0;
   }
   cout << "Move Assignment called on rvalue" << std::endl;
   return *this;
 }
//...snip
};     

Yapıcı ve atama işleçleri, değer referansları alan sürümlerle aşırı yüklenmiştir. Rvalue referansları bir fonksiyonun derleme zamanında (aşırı yük çözünürlüğü yoluyla) "Ben bir değerde mi yoksa bir değerde mi çağrılıyorum?" Bu, kaynakları daha ziyade kopyalamak yerine taşımak için daha verimli kurucu ve atama operatörleri oluşturmamızı sağladı.

Derleyici, derleme zamanında otomatik olarak dallanır (bir değer veya rvalue için çağrılıp çağrılmadığına bağlı olarak) taşıma yapıcısının veya taşıma atama operatörünün çağrılıp çağrılmayacağını seçerek.

Özetle: rvalue referansları, hareket semantiğine (ve aşağıdaki makale bağlantısında tartışılan mükemmel yönlendirmeye) izin verir.

Anlaşılması kolay pratik bir örnek std :: unique_ptr sınıf şablonu . Unique_ptr, temel ham işaretçisinin özel sahipliğini koruduğundan, unique_ptr'ler kopyalanamaz. Bu, münhasır sahiplik değişmezlerini ihlal eder. Yani kopya kurucuları yok. Ancak hareket oluşturucuları var:

template<class T> class unique_ptr {
  //...snip
 unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};

 std::unique_ptr<int[] pt1{new int[10]};  
 std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.  

 // So we must first cast ptr1 to an rvalue 
 std::unique_ptr<int[]> ptr2{std::move(ptr1)};  

std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
 int size)      
{
  for (auto i = 0; i < size; ++i) {
     param[i] += 10;
  }
  return param; // implicitly calls unique_ptr(unique_ptr&&)
}

// Now use function     
unique_ptr<int[]> ptr{new int[10]};

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
           static_cast<unique_ptr<int[]>&&>(ptr), 10);

cout << "output:\n";

for(auto i = 0; i< 10; ++i) {
   cout << new_owner[i] << ", ";
}

output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 

static_cast<unique_ptr<int[]>&&>(ptr)genellikle std :: move kullanılarak yapılır

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);

Tüm bunları ve daha fazlasını açıklayan mükemmel bir makale (değerlerin mükemmel yönlendirmeye nasıl izin verdiği ve bunun ne anlama geldiği gibi) Thomas Becker'in C ++ Rvalue Referanslarının Açıklamasıdır . Bu yazı büyük ölçüde makalesine dayanıyordu.

Daha kısa bir tanıtım rvalue Referanslar BIR kısa giriş et Stroutrup tarafından. ark


Kopya yapıcısının Sample(const Sample& s)içeriği kopyalaması da öyle değil mi? 'Kopya atama operatörü' için de aynı soru.
K.Karamazen

Evet haklısın. Belleği kopyalayamadım. Kopya oluşturucu ve kopya atama işleci, bu boyutu sınadıktan sonra hem memcpy (ptr, s.ptr, size) yapmalı! = 0. Ve varsayılan yapıcı, eğer boyut ise memset (ptr, 0, size) yapmalıdır! = 0.
kurt krueckeberg

Tamam teşekkürler. Böylece bu yorum ve önceki iki yorum kaldırılabilir çünkü problem cevapta da giderilmiştir.
K.Karamazen
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.