Parametreler nasıl doğru bir şekilde aktarılır?


108

Ben bir C ++ acemisiyim ama programlama acemi değilim. C ++ (c ++ 11) öğrenmeye çalışıyorum ve benim için en önemli şey biraz belirsiz: parametreleri geçirme.

Şu basit örnekleri düşündüm:

  • Tüm üyeleri ilkel türlere sahip bir sınıf:
    CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)

  • Üye olarak ilkel türler + 1 karmaşık türe sahip bir sınıf:
    Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)

  • Üye olarak ilkel türler + bazı karmaşık türlerin 1 koleksiyonu olan bir sınıf: Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)

Bir hesap oluşturduğumda şunu yapıyorum:

    CreditCard cc("12345",2,2015,1001);
    Account acc("asdasd",345, cc);

Açıkçası, kredi kartı bu senaryoda iki kez kopyalanacaktır. Bu kurucuyu şu şekilde yeniden yazarsam

Account(std::string number, float amount, CreditCard& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(creditCard)

bir nüsha olacak. Olarak yeniden yazarsam

Account(std::string number, float amount, CreditCard&& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(std::forward<CreditCard>(creditCard))

2 hamle olacak ve kopyası olmayacak.

Sanırım bazen bazı parametreleri kopyalamak isteyebilirsiniz, bazen o nesneyi oluştururken kopyalamak istemezsiniz.
C # 'dan geliyorum ve referanslara alıştığım için, bu bana biraz garip geliyor ve her parametre için 2 aşırı yükleme olması gerektiğini düşünüyorum ama yanıldığımı biliyorum.
C ++ 'da parametrelerin nasıl gönderileceğine dair en iyi uygulamalar var mı, çünkü onu gerçekten buluyorum, diyelim, önemsiz değil. Yukarıda sunulan örneklerimi nasıl ele alırsınız?


9
Meta: Birinin iyi bir C ++ sorusu sorduğuna inanamıyorum. +1.

23
Bilginize, ilkel bir tür değil std::string, tıpkı bir sınıftır CreditCard.
chris

7
Dize karışıklık nedeniyle, ilgisiz olsa, bir dize, bilmelidir ki "abc", tip hariç ucu std::stringdeğil türünden bağımsız olarak char */const char *, ama Çeşidi const char[N](bu durumda N = 4 ile bağlı üç karakterler ve bir null). Bu, yoldan çekilmek için iyi, yaygın bir yanılgıdır.
chris

10
@chuex: Jon Skeet C # sorularının tamamında, çok daha nadiren C ++.
Steve Jessop

Yanıtlar:


158

ÖNCE EN ÖNEMLİ SORU:

C ++ 'da parametrelerin nasıl gönderileceğine dair en iyi uygulamalar var mı, çünkü onu gerçekten buluyorum, diyelim, önemsiz değil

İşlevinizin , iletilen orijinal nesneyi değiştirmesi gerekiyorsa , çağrı geri döndükten sonra, o nesnede yapılan değişiklikler arayan tarafından görülebilir, o zaman lvalue referansına geçmelisiniz :

void foo(my_class& obj)
{
    // Modify obj here...
}

İşlevinizin orijinal nesneyi değiştirmesi gerekmiyorsa ve bunun bir kopyasını oluşturması gerekmiyorsa (başka bir deyişle, yalnızca durumunu gözlemlemesi gerekir), o zaman şunlara lvalue referansıylaconst geçmelisiniz :

void foo(my_class const& obj)
{
    // Observe obj here
}

Bu SolDeğerler ile işlevini hem çağırmak sağlayacak ve SağDeğerler ile (SağDeğerler örneği için vardır (Sol taraf istikrarlı kimliğiyle nesnelerdir) geçicilere veya aradığınız sonucu olarak hareket üzereyiz nesneleri std::move()).

Bir de iddia olabilir kopyalama hızlı olduğu için temel tip veya türlerine gibi int, boolya da char, fonksiyon sadece değeri gözlemlemek için gereken ve eğer referans olarak geçmesine gerek yoktur değeri geçen tercih edilmelidir . Bu, referans semantiğine ihtiyaç duyulmuyorsa doğrudur , ama ya işlev bir yerde aynı girdi nesnesine bir işaretçi depolamak isterse, böylece gelecekte bu işaretçi aracılığıyla okuma, öğenin başka bir bölümünde gerçekleştirilen değer değişikliklerini görecektir. kod? Bu durumda, referansla geçmek doğru çözümdür.

İşlevinizin orijinal nesneyi değiştirmesi gerekmiyorsa, ancak bu nesnenin bir kopyasını saklaması gerekiyorsa ( muhtemelen girdiyi değiştirmeden girdinin dönüştürülmesinin sonucunu döndürmek için ), o zaman değer olarak almayı düşünebilirsiniz :

void foo(my_class obj) // One copy or one move here, but not working on
                       // the original object...
{
    // Working on obj...

    // Possibly move from obj if the result has to be stored somewhere...
}

Yukarıdaki işlevi çağırmak, her zaman ldeğerleri geçerken bir kopya ve r değerleri geçerken bir hamle ile sonuçlanacaktır. Fonksiyonun bu nesne bir yere saklamak gerekiyorsa, ek bir gerçekleştirebilir hareket ondan (örneğin durumda foo()olduğunu ihtiyaçlar veri üyesi değeri saklamak için bir üye fonksiyonu ).

Türdeki nesneler için hareketlerin pahalı olması durumundamy_class , aşırı yüklemeyi düşünebilir foo()ve ldeğerler için bir sürüm (bir ldeğer başvurusunu kabul ederek const) ve r değerleri için bir sürüm (bir r değeri başvurusunu kabul ederek) sağlayabilirsiniz:

// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
    my_class copyOfObj = obj; // Copy!
    // Working on copyOfObj...
}

// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
    my_class copyOfObj = std::move(obj); // Move! 
                                         // Notice, that invoking std::move() is 
                                         // necessary here, because obj is an
                                         // *lvalue*, even though its type is 
                                         // "rvalue reference to my_class".
    // Working on copyOfObj...
}

Yukarıdaki işlevler o kadar benzer ki, ondan tek bir işlev yapabilirsiniz: foo()bir işlev şablonu olabilir ve geçirilen nesnenin bir hareketinin veya bir kopyasının dahili olarak oluşturulup oluşturulmayacağını belirlemek için mükemmel iletmeyi kullanabilirsiniz :

template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
//       ^^^
//       Beware, this is not always an rvalue reference! This will "magically"
//       resolve into my_class& if an lvalue is passed, and my_class&& if an
//       rvalue is passed
{
    my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
    // Working on copyOfObj...
}

Scott Meyers'in bu konuşmasını izleyerek bu tasarım hakkında daha fazla bilgi edinmek isteyebilirsiniz (kullandığı " Evrensel Referanslar " teriminin standart dışı olduğunu unutmayın).

Akılda tutulması gereken bir şey, bunun std::forwardgenellikle rdeğerler için bir hareketle sonuçlanacağıdır , bu nedenle nispeten masum görünse de, aynı nesneyi birden çok kez iletmek bir sorun kaynağı olabilir - örneğin, aynı nesneden iki kez hareket etmek! Bu nedenle, bunu bir döngüye koymamaya ve aynı argümanı bir işlev çağrısında birden çok kez iletmemeye dikkat edin:

template<typename C>
void foo(C&& obj)
{
    bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}

Ayrıca, iyi bir nedeniniz olmadıkça normalde şablon tabanlı çözüme başvurmadığınıza dikkat edin, çünkü bu kodunuzun okunmasını zorlaştırır. Normalde açıklığa ve basitliğe odaklanmalısınız .

Yukarıdakiler sadece basit yönergelerdir, ancak çoğu zaman sizi iyi tasarım kararlarına yönlendireceklerdir.


YAYININIZIN KALANLARI İLE İLGİLİ

[...] olarak yeniden yazarsam 2 hareket olur ve kopyası olmaz.

Bu doğru değil. Başlangıç ​​olarak, bir rvalue başvurusu bir lvalue'ya bağlanamaz, bu nedenle bu yalnızca kurucunuza bir rvalue türü geçirdiğinizde derlenir CreditCard. Örneğin:

// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));

CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));

Ama bunu yapmaya çalışırsan işe yaramayacak:

CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue

Çünkü ccbir lvalue ve rvalue referansları ldeğerlere bağlanamaz. Dahası, bir nesneye bir referans bağlarken hiçbir hareket gerçekleştirilmez : bu sadece bir referans bağlamadır. Böylece, sadece bir hareket olacaktır.


Bu nedenle, bu cevabın ilk bölümünde verilen yönergelere göre, bir CreditCarddeğer olarak aldığınızda üretilen hareket sayısıyla ilgileniyorsanız , biri const( CreditCard const&) ' ye bir ldeğer referansı ve diğeri alarak iki kurucu aşırı yüklemesi tanımlayabilirsiniz. bir rvalue referansı ( CreditCard&&).

Aşırı yük çözünürlüğü, bir ldeğer geçerken ilkini seçer (bu durumda, bir kopya gerçekleştirilir) ve ikincisi, bir r değeri geçerken (bu durumda, bir hareket gerçekleştirilir).

Account(std::string number, float amount, CreditCard const& creditCard) 
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }

Account(std::string number, float amount, CreditCard&& creditCard) 
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }

Kullanımınız std::forward<>, normalde mükemmel bir yönlendirme elde etmek istediğinizde görülür . Bu durumda, kurucunuz aslında bir yapıcı şablon olur ve aşağı yukarı aşağıdaki gibi görünür

template<typename C>
Account(std::string number, float amount, C&& creditCard) 
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }

Bir anlamda, bu, daha önce gösterdiğim her iki aşırı yüklemeyi tek bir işlevde birleştirir: bir l Cdeğeri CreditCard&geçmeniz durumunda olduğu çıkarılır ve referans çökme kuralları nedeniyle, bu işlevin somutlaştırılmasına neden olur:

Account(std::string number, float amount, CreditCard& creditCard) : 
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard)) 
{ }

Bu neden olacaktır kopyalamaya karşı inşaat ve creditCardistediğiniz gibi. Öte yandan, bir rvalue iletildiğinde, Colduğu çıkarılır CreditCardve bunun yerine bu işlev başlatılır:

Account(std::string number, float amount, CreditCard&& creditCard) : 
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard)) 
{ }

Bu neden olacaktır hareket-inşaat ve creditCard(geçirilen değer bir rvalue olduğunu ve araçlarının biz ondan hareket etmek yetkisine sahiptir çünkü) ne istediğinizi olan.


İlkel olmayan parametre türleri için şablon versiyonunun her zaman forward ile kullanılmasının uygun olduğunu söylemek yanlış mıdır?
Jack Willson

1
@JackWillson: Sadece hareketlerin performansı konusunda gerçekten endişeleriniz olduğunda şablon versiyonuna başvurmanız gerektiğini söyleyebilirim. Daha fazla bilgi için iyi bir cevabı olan bu soruma bakın . Genel olarak, bir kopya oluşturmanız gerekmiyorsa ve yalnızca gözlemlemeniz gerekiyorsa, ref const. Orijinal nesneyi değiştirmeniz gerekirse, ref ile non -const'u alın. Bir kopya almanız gerekiyorsa ve hamleler ucuzsa, değere göre alın ve sonra hareket edin.
Andy Prowl

2
Üçüncü kod örneğinde hareket etmeye gerek olmadığını düşünüyorum. objTaşınmak için yerel bir kopya yapmak yerine kullanabilirsiniz , değil mi?
juanchopanza

3
@AndyProwl: +1 saat önce aldınız, ancak (mükemmel) cevabınızı yeniden okurken iki şeye dikkat çekmek istiyorum: a) değer her zaman bir kopya oluşturmaz (eğer taşınmamışsa). Nesne, arayanların sitesinde, özellikle RVO / NRVO ile yerinde inşa edilebilir, hatta bu, birinin ilk düşündüğünden daha sık çalışır. b) Lütfen son örnekte yalnızca bir kezstd::forward çağrılabileceğini belirtiniz . İnsanların bunu döngülere koyduğunu gördüm ve bu yanıt birçok yeni başlayan tarafından görüleceği için, bu tuzaktan kaçınmalarına yardımcı olmak için IMHO'nun şişman bir "Uyarı!" Etiketi olması gerekir.
Daniel Frey

1
@SteveJessop: Ve bunun dışında, iletme işlevleriyle ilgili teknik sorunlar (sorun olması gerekmez) ( özellikle tek bir argüman alan kurucular ), çoğunlukla herhangi bir türden argümanı kabul std::is_constructible<>etmeleri ve uygun şekilde SFINAE olmadıkları sürece tip özelliğini yenebilirler. kısıtlı - bu bazıları için önemsiz olmayabilir.
Andy Prowl

11

Önce bazı ayrıntıları düzeltmeme izin verin. Aşağıdakileri söylediğinizde:

2 hareket olacak ve kopyası olmayacak.

Bu yanlış. Bir rvalue referansına bağlanmak bir hareket değildir. Tek bir hareket var.

Ek olarak, CreditCardbir şablon parametresi olmadığı için std::forward<CreditCard>(creditCard), söylemenin sadece ayrıntılı bir yoludur std::move(creditCard).

Şimdi ...

Tiplerinizin "ucuz" hareketleri varsa, hayatınızı kolaylaştırmak ve her şeyi değerine göre ve " std::movebirlikte" almak isteyebilirsiniz .

Account(std::string number, float amount, CreditCard creditCard)
: number(std::move(number),
  amount(amount),
  creditCard(std::move(creditCard)) {}

Bu yaklaşım, yalnızca bir tane verebileceği zaman size iki hamle verir, ancak hamleler ucuzsa, kabul edilebilir olabilirler.

Biz bu "ucuz hamleler" meselesi üzerindeyken, size bunun std::stringgenellikle sözde küçük dizgi optimizasyonu ile uygulandığını hatırlatmalıyım , bu nedenle hamleleri bazı işaretçileri kopyalamak kadar ucuz olmayabilir. Her zamanki gibi optimizasyon sorunları söz konusu olduğunda, önemli olsun ya da olmasın, bana değil profilcinize sormanız gereken bir şey.

Bu ekstra hareketlere katlanmak istemiyorsanız ne yapmalısınız? Belki çok pahalı olduklarını kanıtlıyorlar veya daha kötüsü, belki türler aslında taşınamıyor ve fazladan kopyalara maruz kalabilirsiniz.

Yalnızca bir sorunlu parametre varsa, T const&ve ile iki aşırı yükleme sağlayabilirsiniz T&&. Bu, bir kopya veya taşıma işleminin gerçekleştiği asıl üye başlatılana kadar her zaman referansları bağlayacaktır.

Bununla birlikte, birden fazla parametreniz varsa, bu aşırı yüklerin sayısında üstel bir patlamaya yol açar.

Bu mükemmel yönlendirme ile çözülebilecek bir sorundur. Bu, bunun yerine bir şablon yazmanız std::forwardve argümanların değer kategorisini üye olarak nihai hedeflerine taşımak için kullanmanız anlamına gelir .

template <typename TString, typename TCreditCard>
Account(TString&& number, float amount, TCreditCard&& creditCard)
: number(std::forward<TString>(number),
  amount(amount),
  creditCard(std::forward<TCreditCard>(creditCard)) {}

Şablon sürümüyle ilgili bir sorun var: kullanıcı Account("",0,{brace, initialisation})artık yazamıyor .
ipc

@ipc ah, doğru. Bu gerçekten can sıkıcı ve ölçeklenebilir kolay bir çözüm olduğunu düşünmüyorum.
R. Martinho Fernandes

6

Her şeyden önce std::stringoldukça ağır bir sınıf türü std::vector. Kesinlikle ilkel değil.

Bir kurucuya değere göre herhangi bir büyük taşınabilir türü alıyorsanız, std::movebunları üyeye yapardım :

CreditCard(std::string number, float amount, CreditCard creditCard)
  : number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }

Yapıcıyı tam olarak bu şekilde uygulamayı tavsiye ederim. Bu üyeler sebep numberve creditCardolmaya hareket yerine kopya inşa daha inşa edilmiştir. Bu kurucuyu kullandığınızda, nesne kurucuya aktarılırken bir kopya (veya geçici ise taşıma) ve ardından üye başlatılırken bir hareket olacaktır.

Şimdi bu kurucuyu ele alalım:

Account(std::string number, float amount, CreditCard& creditCard)
  : number(number), amount(amount), creditCard(creditCard)

Haklısın, bu bir kopyasını içerecek creditCard, çünkü ilk önce kurucuya referansla aktarılıyor. Ancak artık constnesneleri kurucuya geçiremezsiniz (çünkü referans non- const) ve geçici nesneleri geçiremezsiniz. Örneğin, bunu yapamazsınız:

Account account("something", 10.0f, CreditCard("12345",2,2015,1001));

Şimdi düşünelim:

Account(std::string number, float amount, CreditCard&& creditCard)
  : number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))

Burada rvalue referanslarının yanlış anlaşıldığını gösterdiniz ve std::forward. Gerçekten sadece std::forwardyönlendirdiğiniz nesne, çıkarılmış bir türT&& için olduğu gibi bildirildiğinde kullanmalısınız . Burada çıkarılmamış (varsayıyorum) ve bu yüzden yanlış kullanılıyor. Yukarı bak evrensel referanslar . TCreditCardstd::forward


1

Genel durum için oldukça basit bir kural kullanıyorum: POD için copy (int, bool, double, ...) ve diğer her şey için const & kullanın ...

Kopyalamak ya da istememek, yöntemin imzasıyla değil, daha çok parametrelerle yaptığınız şeyle yanıtlanır.

struct A {
  A(const std::string& aValue, const std::string& another) 
    : copiedValue(aValue), justARef(another) {}
  std::string copiedValue;
  const std::string& justARef; 
};

işaretçi için hassasiyet: Neredeyse hiç kullanmadım. & 'E göre tek avantajı boş veya yeniden atanabilmeleridir.


2
"Genel durum için oldukça basit bir kural kullanıyorum: POD (int, bool, double, ...) için copy ve diğer her şey için const & kullanın." Hayır. Sadece Hayır.
Ayakkabı

Bir & (sabit yok) kullanarak bir değeri değiştirmek istiyorsanız ekliyor olabilir. Aksi halde görmüyorum ... basitlik yeterince iyi. Ama öyle
dediyseniz

Bazen ilkel türlere göre değişiklik yapmak istersiniz ve bazen nesnelerin bir kopyasını oluşturmanız gerekir ve bazen nesneleri taşımanız gerekir. Her şeyi değere göre POD> ve referans olarak UDT> 'ye indirgeyemezsiniz.
Ayakkabı

Tamam, bu sadece sonradan ekledim. Çok hızlı bir cevap olabilir.
David Fleury

1

Benim için en önemli şey biraz belirsiz: parametrelerin aktarılması.

  • İşlev / yöntem içinde iletilen değişkeni değiştirmek istiyorsanız
    • referansla geçtin
    • bir işaretçi olarak geçirirsin (*)
  • İşlev / yöntem içinde aktarılan değeri / değişkeni okumak istiyorsanız
    • const referansı ile geçersiniz
  • İşlev / yöntem içinde iletilen değeri değiştirmek istiyorsanız
    • normal olarak nesneyi kopyalayarak geçersiniz (**)

(*) işaretçiler, dinamik olarak ayrılmış belleğe atıfta bulunabilir, bu nedenle mümkün olduğunda, referanslar sonunda genellikle işaretçiler olarak uygulanmış olsa bile, referansları işaretçiler yerine tercih etmelisiniz.

(**) "normal olarak", kopya yapıcı (aynı parametre türünden bir nesne iletirseniz) veya normal kurucu (sınıf için uyumlu bir tür iletirseniz) anlamına gelir. myMethod(std::string)Örneğin, bir nesneyi std::stringilettiğinizde, ona bir iletilirse kopya yapıcı kullanılacaktır , bu nedenle var olduğundan emin olmanız gerekir.

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.