Std :: forward kullanmanın temel amaçları nelerdir ve hangi sorunları çözer?


438

Mükemmel iletmede, std::forwardadlandırılmış değer referanslarını t1ve t2adlandırılmamış değer referanslarına dönüştürmek için kullanılır . Bunu yapmanın amacı nedir? Bu nasıl denir fonksiyonu etkileyecek innerGidersek t1& t2SolDeğerler olarak?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Yanıtlar:


789

Yönlendirme sorununu anlamalısınız. Şunları yapabilirsiniz tamamını ayrıntılı bir sorunu okumak , ama özetlemek gerekir.

Temel olarak, ifade verildiğinde, ifadenin eşdeğer olmasını E(a, b, ... , c)istiyoruz f(a, b, ... , c). C ++ 03'te bu mümkün değildir. Birçok deneme var, ama hepsi bir bakıma başarısız oluyor.


En basit olanı bir değer referansı kullanmaktır:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Ancak bu geçici değerleri işleyemez: f(1, 2, 3);çünkü bunlar bir değer referansına bağlanamaz.

Bir sonraki girişim şunlar olabilir:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Bu yukarıdaki sorunu giderir, ancak flopları çevirir. Artık Econst olmayan bağımsız değişkenlere izin verilmiyor:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

Üçüncü girişim const-referansları kabul eder ama sonra const_cast's constuzak:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Bu, tüm değerleri kabul eder, tüm değerleri aktarabilir, ancak potansiyel olarak tanımlanmamış davranışa yol açar:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Nihai bir çözüm her şeyi doğru bir şekilde işler ... bakımı imkansız olmak pahasına. Sen aşırı yükleri sağlamak file, tüm const olmayan const kombinasyonları:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N argümanları 2 N kombinasyonu, bir kabus gerektirir . Bunu otomatik olarak yapmak istiyoruz.

(Derleyicinin C ++ 11'de bizim için yapmasını sağladığımız şey budur.)


C ++ 11'de bunu düzeltme şansımız var. Bir çözüm, mevcut türlerde şablon kesinti kurallarını değiştirir, ancak bu potansiyel olarak büyük miktarda kodu ihlal eder. Bu yüzden başka bir yol bulmalıyız.

Çözüm bunun yerine yeni eklenen rvalue referanslarını kullanmaktır ; değer referansı türlerini çıkarırken yeni kurallar getirebilir ve istenen sonuçları yaratabiliriz. Sonuçta, şimdi kodu kıramayacağız.

Bir referansa referans verildiyse (not referansı hem T&ve hem de anlamını kapsayan bir terimdir T&&), ortaya çıkan türü bulmak için aşağıdaki kuralı kullanırız:

"[verilen] T türüne referans olan bir TR türü," cv TR'ye lvalue referansı türünü yaratma girişimi "T'ye lvalue referansı türünü yaratırken," rvalue referansı türünü yaratma girişimi " cv TR ”, TR türünü oluşturur."

Veya tablo şeklinde:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Daha sonra, şablon argümanı kesinti ile: bir argüman bir A değeri ise, şablon bağımsız değişkenine A'ya bir değer referansı sağlarız. Aksi takdirde, normal olarak çıkarırız. Bu, sözde evrensel referanslar verir ( yönlendirme referansı terimi şimdi resmi olan).

Bu neden faydalı? Birleştirildiğinden, bir türün değer kategorisini takip etme yeteneğimizi koruduğumuzdan: eğer bir değerse, bir değer değeri referans parametresimiz var, aksi takdirde bir değer değeri referans parametresimiz var.

Kodda:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Son şey, değişkenin değer kategorisini "iletmektir". Unutmayın, fonksiyonun içine girdikten sonra parametre herhangi bir değere lvalue olarak geçirilebilir:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Bu iyi değil. E'nin sahip olduğumuz değer kategorisini alması gerekiyor! Çözüm şudur:

static_cast<T&&>(x);

Bu ne yapar? deduceİşlevin içinde olduğumuzu düşünün ve bize bir değer verildi. Bu T, a anlamına gelir A&ve bu nedenle statik yayın için hedef türü A& &&veya adil olur A&. Yana xzaten bir A&hiçbir şey yapamaz ve bir lvalue referansla bırakılır.

Biz rvalue geçirilen oldum zaman Tolduğu Astatik döküm hedef türüdür yüzden A&&. Döküm, artık bir değer referansına geçirilemeyen bir değerlik ifadesiyle sonuçlanır . Parametrenin değer kategorisini koruduk.

Bunları bir araya getirmek bize "mükemmel yönlendirme" sağlar:

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Bir flvalue aldığında, bir lvalue Ealır. Bir frvalue aldığında, bir rvalue Ealır. Mükemmel.


Ve elbette çirkinlerden kurtulmak istiyoruz. static_cast<T&&>şifreli ve hatırlanması garip; bunun yerine forwardaynı şeyi yapan bir yardımcı fonksiyon yapalım :

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

1
Olmaz fbir işlev değil, bir ifade olabilir mi?
Michael Foukarakis

1
Son girişiminiz sorun bildirimi ile ilgili olarak doğru değildir: Sabit değerleri sabit olmayan olarak iletir, böylece hiç iletmez. Ayrıca ilk denemede, const int ikabul edilecektir: Açıkarılır const int. Başarısızlıklar rvalues ​​değişmez değerleri içindir. Ayrıca arama için dikkat deduced(1), x int&&, değil int(eğer yapıldığı gibi kusursuz yönlendirme, bir kopyasını yapar asla xbir yan değere parametre olacaktır). Sadece Töyle int. xİleticideki bir değeri değerlendirmenin nedeni, adlandırılmış değer referanslarının değer ifadeleri haline gelmesidir.
Johannes Schaub - litb

5
Burada forwardveya moveburada kullanımda herhangi bir fark var mı? Yoksa sadece anlamsal bir fark mı?
0x499602D2

28
@David: std::moveaçık şablon argümanları olmadan çağrılmalı ve her zaman bir rvalue ile sonuçlanırken, std::forwardikisi de olabilir. Kullanım std::moveartık biliyorum değerine ihtiyacımız ve başka bir yere taşımak istiyorum, kullanım std::forwardyapmak senin işlev şablonuna geçirilen değerlere göre.
GManNickG

5
Önce somut örneklerle başladığınız ve sorunu motive ettiğiniz için teşekkürler; çok yararlı!
ShreevatsaR

61

Ben std :: forward uygulamak kavramsal bir kod olması düşünüyorum tartışmaya ekleyebilirsiniz. Bu Scott Meyers bir slayt konuşma etkili bir C ++ 11/14 Sampler

kavramsal kod uygulama std :: ileri

İşlev movekodu gönderiyor std::move. Bu konuşmada daha önce bunun için (çalışan) bir uygulama var. Bulduğum std gerçek uygulanmasını :: ileri ++ libstdc içinde , dosya move.h içinde, ama hepsi öğretici de değil.

Bir kullanıcının bakış açısından, bunun anlamı, std::forwardbir rvalue için koşullu bir dökümdür. Bir parametrede bir lvalue veya rvalue bekleyen ve yalnızca rvalue olarak geçirilmişse bir rvalue olarak başka bir işleve geçirmek isteyen bir işlev yazıyorsam faydalı olabilir. Parametreyi std :: forward içine sarmasaydım, her zaman normal referans olarak geçilirdi.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Tabii ki, yazdırıyor

std::string& version
std::string&& version

Kod, daha önce bahsedilen konuşmadan bir örneğe dayanmaktadır. 10'u en başından yaklaşık 15: 00'te kaydırın.


2
İkinci bağlantınız tamamen farklı bir yere işaret etti.
Pharap

34

Mükemmel iletmede, std :: forward, belirtilen t1 ve t2 değer referansını adlandırılmamış değer referansına dönüştürmek için kullanılır. Bunu yapmanın amacı nedir? Eğer t1 & t2 değerini lvalue olarak bırakırsak, bu işlev denilen fonksiyonu nasıl etkiler?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

İfadede adlandırılmış bir değer başvurusu kullanırsanız, bu aslında bir değerdir (çünkü nesneye ada göre başvurursunuz). Aşağıdaki örneği düşünün:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Şimdi, ararsanız outerböyle

outer(17,29);

17 ve 29'un # 2'ye iletilmesini istiyoruz, çünkü 17 ve 29 tamsayı değişmez değerler ve bu tür değerler. Ama bu yana t1ve t2ifadede inner(t1,t2);# 1 yerine 2. çağırma olurdum Sol taraf vardır. Bu yüzden referansları yeniden adlandırılmamış referanslara dönüştürmeliyiz std::forward. Yani, t1içinde outeriken bir lvalue ifadesi her zaman forward<T1>(t1)bir rvalue ifadesi bağlı olabilir T1. İkincisi, yalnızca bir değer T1referansı olduğunda lvalue ifadesidir . Ve T1sadece dışa yönelik ilk argümanın bir lvalue ifadesi olması durumunda bir lvalue referansı olduğu sonucuna varılır.


Bu bir tür sulandırılmış açıklamadır, ancak çok iyi yapılmış ve fonksiyonel bir açıklamadır. İnsanlar önce bu cevabı okumalı ve sonra daha derine
inmeli

@sellibitze int a; f (a): "a bir değer olduğu için int (T &&) int (int & &&) ile eşittir" veya "T && yani T int olmalı & "? İkincisini tercih ederim.
John

11

Eğer t1 & t2 değerini lvalue olarak bırakırsak, bu işlev denilen fonksiyonu nasıl etkiler?

, Başlatıldıktan sonra ise T1tiptedir charve T2bir sınıfın, sen geçirmek istediğiniz t1kopya başına ve t2başına constreferans. Eh, inner()onları constreferans olmayanlara göre almadıkça , yani bu durumda da yapmak istersiniz.

outer()Rvalue referansları olmadan bunu uygulayan bir dizi işlev yazmaya çalışın ve argümanları inner()türünden geçirmenin doğru yolunu bulun . Bence 2 ^ 2 bir şey, argümanları çıkarmak için oldukça iri şablon meta malzeme ve tüm durumlar için bu doğru almak için çok zaman gerekir.

Ve sonra biri inner()işaretçi başına argüman alan bir ile birlikte gelir . Sanırım şimdi 3 ^ 2 yapıyor. (Ya da 4 ^ 2. Cehennem, constimlecin bir fark yaratıp yaratmayacağını düşünmeye zahmet etmem .)

Ve sonra bunu beş parametre için yapmak istediğinizi düşünün. Veya yedi.

Şimdi neden bazı parlak zihinlerin "mükemmel yönlendirme" ile geldiğini biliyorsunuz: Bu derleyiciyi sizin için yapıyor.


5

Kristal netleştirilmemiş bir nokta da düzgün bir şekilde static_cast<T&&>ele alınmasıdır const T&.
Programı:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

üretir:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

'F' nin bir şablon işlevi olması gerektiğini unutmayın. Eğer sadece 'void f (int && a)' olarak tanımlanmışsa, bu işe yaramaz.


iyi nokta, bu yüzden statik dökümde T && da referans daraltma kurallarına uyuyor, değil mi?
barney

3

Yönlendirmenin, yönlendirme / evrensel referanslı bir dış yöntemle birlikte kullanılması gerektiğini vurgulamak faydalı olabilir. Aşağıdaki ifadeler olarak ileriye doğru kendi başına kullanılmasına izin verilir, ancak karışıklığa neden olmaktan başka hiçbir faydası yoktur. Standart komite bu esnekliği devre dışı bırakmak isteyebilir, aksi halde neden static_cast kullanmıyoruz?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

Kanımca, ileriye doğru hareket ve ileri, r-değeri referans türü tanıtıldıktan sonra doğal sonuçlar olan tasarım örüntüleridir. Yanlış kullanım yasaklanmadığı sürece doğru kullanıldığını varsayarak bir yöntem adlandırmamalıyız.


C ++ komitesinin kendilerinin dil deyimlerini "doğru" kullanmalarını, hatta "doğru" kullanımın ne olduğunu tanımlamasını (kesinlikle yönerge verebilirler) hissettiklerini sanmıyorum. Bu amaçla, bir kişinin öğretmenleri, patronları ve arkadaşları onları bir şekilde yönlendirmekle yükümlü olabilirken, C ++ komitesinin (ve dolayısıyla standardın) bu görevi olmadığına inanıyorum.
SirGuy

Evet, sadece N2951'i okudum ve standart komitenin bir fonksiyonun kullanımı ile ilgili gereksiz sınırlamalar eklemekle yükümlü olmadığını kabul ediyorum. Ancak bu iki işlev şablonunun (taşıma ve ileri alma) adları, yalnızca kütüphane dosyasındaki tanımlarını veya standart belgelerdeki (23.2.5 İleri / taşıma yardımcıları) görmeleri biraz kafa karıştırıcıdır. Standarttaki örnekler kesinlikle kavramı anlamaya yardımcı olur, ancak işleri biraz daha açık hale getirmek için daha fazla açıklama eklemek yararlı olabilir.
colin
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.