Const std :: string & parametrelerini geçen günler parametre olarak mı?


604

Herb Sutter tarafından geçen std::vectorve geçen nedenlerin büyük ölçüde gittiğini öneren bir konuşma duydum . Aşağıdaki gibi bir işlev yazmanın artık tercih edilebilir olduğunu öne sürdü:std::stringconst &

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

return_valİşlevin döndüğü noktada bir rvalue olacağını ve bu nedenle çok ucuz olan hareket semantiği kullanılarak döndürülebileceğini anlıyorum . Bununla birlikte, invalhala bir referansın boyutundan çok daha büyüktür (genellikle bir işaretçi olarak uygulanır). Bunun nedeni std::string, a'nın öbeğe bir işaretçi ve char[]kısa dize optimizasyonu için bir üye dahil olmak üzere çeşitli bileşenlere sahip olmasıdır . Bana öyle geliyor ki, referansla geçmenin hala iyi bir fikir olduğu anlaşılıyor.

Herb neden bunu söylemiş olabilir?


89
Sorunun en iyi cevabının muhtemelen Dave Abrahams'ın C ++ Next ile ilgili makalesini okumak olduğunu düşünüyorum . Bu konuda konu dışı veya yapıcı olmayan bir şey görmediğimi de eklerim. Bu, somut cevapların olduğu programlama hakkında açık bir soru.
Jerry Coffin

Büyüleyici, bu yüzden yine de bir kopya yapmak zorunda kalacaksanız, by-pass değeri büyük olasılıkla pass-by-referansından daha hızlıdır.
Benj

3
@Sz. Yanlışlıkla kopyalar ve kapalı olarak sınıflandırılan sorulara karşı hassasım. Bu davanın detaylarını hatırlamıyorum ve bunları tekrar incelemedim. Bunun yerine, sadece bir hata yaptığım varsayımı hakkındaki yorumumu sileceğim. Bu hususları dikkatime sunduğunuz için teşekkür ederim.
Howard Hinnant

2
@HowardHinnant, çok teşekkür ederim, bu özen ve duyarlılık seviyesine rastlayan her zaman değerli bir an, çok ferahlatıcı! (O zaman tabii ki benimkini sileceğim.)
Sz.

Yanıtlar:


393

Herb'in söylediklerinin nedeni bu gibi durumlar.

Diyelim ki işlevi Açağıran bir işlevim var B, işlevi çağıran C. Ve Abir ipi içine Bve içine geçirir C. Abilmiyor veya önemsemiyor C; hakkında her şey Abilir B. Yani, Cbir uygulama detayıdır B.

A'nın aşağıdaki gibi tanımlandığını varsayalım:

void A()
{
  B("value");
}

B ve C dizeyi alırsa const&, şöyle görünür:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Her şey yolunda ve güzel. Sadece işaretçiler geçiyorsunuz, kopyalama yok, hareket yok, herkes mutlu. Calır const&çünkü dizeyi saklamaz. Sadece kullanır.

Şimdi, basit bir değişiklik yapmak istiyorum: Cdizeyi bir yerde saklamak gerekiyor.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

Merhaba, kopya oluşturucu ve potansiyel bellek ayırma ( Kısa Dize Optimizasyonu (SSO) yoksay ). C ++ 11'in hareket semantiği gereksiz kopya oluşturmayı kaldırmayı mümkün kılıyor, değil mi? Ve Ageçici geçiyor; verileri kopyalamakC zorunda olmanız için hiçbir neden yoktur . Ona verilen şeyden kaçınmalı.

Dışında olamaz. Çünkü bir const&.

Eğer Cparametresini değere göre değiştirirsem , bu sadece Bbu parametreye kopyalamaya neden olur ; Hiçbir şey kazanmıyorum.

Bu yüzden str, tüm fonksiyonlar boyunca değerden geçmiş olsaydım std::move, verileri karıştırmaya güveniyor olsaydım , bu problemimiz olmazdı. Birisi ona tutunmak istiyorsa, yapabilir. Eğer yapmazlarsa, oh iyi.

Daha mı pahalı? Evet; bir değere geçmek referans kullanmaktan daha pahalıdır. Kopyadan daha ucuz mu? TOA içeren küçük teller için değil. Yapmaya değer mi?

Kullanım durumunuza bağlıdır. Bellek ayırmalarından ne kadar nefret ediyorsunuz?


2
Bir değere taşınmanın referansları kullanmaktan daha pahalı olduğunu söylediğinizde, bu sabit bir miktarda (taşınan dizenin uzunluğundan bağımsız olarak) hala daha pahalıdır değil mi?
Neil G

3
@NeilG: "Uygulamaya bağlı" nın ne anlama geldiğini anlıyor musunuz? Söyledikleriniz yanlış çünkü SSO'nun uygulanıp uygulanmadığına ve nasıl uygulandığına bağlı.
ildjarn

17
@ildjarn: Sipariş analizinde, bir şeyin en kötü durumu bir sabitle bağlıysa, o zaman hala sabit bir zamandır. En uzun küçük bir ip yok mu? Bu dizenin kopyalanması sabit bir süre almıyor mu? Tüm küçük dizelerin kopyalanması daha az zaman almaz mı? Daha sonra, küçük dizeler için dize kopyalama, sipariş analizinde "sabit zaman" olur - küçük dizelerin kopyalanması için çok fazla zaman harcanmasına rağmen. Sıra analizi asimtotik davranışla ilgilidir .
Neil G

8
@NeilG: Ama asıl sorunuz şu: " bu sabit bir miktar daha pahalı (taşınan ipin uzunluğundan bağımsız olarak) değil mi? " Yapmaya çalıştığım nokta, farklı "no" olarak özetlenen dizenin uzunluğuna bağlı olarak sabit tutarlar.
ildjarn

13
Neden movedby by durumunda dize B'den C'ye olsun? Eğer B B(std::string b)ve C ise, C(std::string c)ya C(std::move(b))B'yi aramalıyız ya bda çıkana kadar değişmeden kalmalıyız (böylece 'taşınmamış') B. (Belki bir optimize derleyici altında dize hareket edecek şekilde -eğer kural eğer bçağrıdan sonra kullanılmaz ama güçlü bir garantisi yoktur sanmıyorum.) Aynısının kopyası için geçerlidir stretmek m_str. Bir işlev parametresi bir değerle başlatılmış olsa bile, bu işlev içindeki bir değerdir ve std::movebu değerden hareket etmek gerekir.
Pixelchemist

163

Const std :: string & parametrelerini geçen günler parametre olarak mı?

Hayır . Birçok kişi bu tavsiyeyi (Dave Abrahams dahil) uygulandığı alan adının ötesine taşır ve tüm std::string parametrelere uygulanmasını basitleştirir - Her zamanstd::string değerden geçmek , keyfi parametreler ve uygulamalar için her zaman "en iyi uygulama" değildir çünkü optimizasyonlar görüşmeler / makaleler sadece sınırlı sayıda vaka için geçerlidir .

Bir değer döndürüyorsanız, parametreyi değiştiriyorsanız veya değeri alıyorsanız, değere göre geçmek pahalı kopyalamayı kurtarabilir ve sözdizimi kolaylığı sağlayabilir.

Her zamanki gibi, const referansından geçmek, bir kopyaya ihtiyacınız olmadığında çok fazla kopyadan tasarruf sağlar .

Şimdi özel örneğe bakalım:

Bununla birlikte, inval hala bir referansın boyutundan (genellikle bir işaretçi olarak uygulanır) çok daha büyüktür. Bunun nedeni, bir std :: string'in, öbeğe bir işaretçi ve kısa dize optimizasyonu için bir üye char [] dahil çeşitli bileşenlere sahip olmasıdır. Bana öyle geliyor ki, referansla geçmenin hala iyi bir fikir olduğu anlaşılıyor. Herb neden bunu söylemiş olabilir?

Yığın boyutu bir endişe ise (ve bunun satır içi / optimize edilmediğini varsayarak), return_val+ inval> return_val- IOW ise, pik yığın kullanımı, değere buradan geçirilerek azaltılabilir (not: ABI'lerin aşırı basitleştirilmesi). Bu arada, const referansı ile geçmek optimizasyonları devre dışı bırakabilir. Buradaki ana neden, yığın büyümesini önlemek değil, optimizasyonun uygulanabilir olduğu yerde gerçekleştirilebilmesini sağlamaktır .

Const referansı ile geçen günler bitmedi - kurallar eskisinden daha karmaşık. Performans önemliyse, uygulamalarınızda kullandığınız ayrıntılara dayanarak bu türleri nasıl geçireceğinizi düşünmek akıllıca olacaktır.


3
Yığın kullanımında, tipik ABI'ler yığın kullanımı olmayan bir kayıtta tek bir referans geçirir.
ahcox

63

Bu büyük ölçüde derleyicinin uygulamasına bağlıdır.

Ancak, ne kullandığınıza da bağlıdır.

Bir sonraki fonksiyonları ele alalım:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

Bu işlevler, satır içi çizgiden kaçınmak için ayrı bir derleme biriminde uygulanır. O zaman:
1. Eğer bu iki fonksiyona gerçek bir bilgi aktarırsanız, performanslarda fazla bir fark görmezsiniz. Her iki durumda da, bir dize nesnesi oluşturulmalıdır
2. Başka bir std :: string nesnesi iletirseniz, foo2daha iyi performans gösterir foo1, çünkü foo1derin bir kopya yapar.

Bilgisayarımda g ++ 4.6.1 kullanarak şu sonuçları aldım:

  • referans ile değişken: 1000000000 iterasyon -> geçen süre: 2.25912 sn
  • değere göre değişken: 1000000000 iterasyon -> geçen süre: 27.2259 sn
  • referans ile değişmez değer: 100000000 iterasyon -> geçen süre: 9.10319 sn
  • değere göre değişmez değer: 100000000 iterasyon -> geçen süre: 8.62659 sn

4
Daha alakalı olan , işlevin içinde olup biten şeydir : bir referansla çağrılırsa, değerin içinden geçerken atlanabilecek bir kopyasını dahili olarak yapması gerekir mi?
leftaroundabout

1
@leftaroundabout Evet, rota dışı. Her iki işlevin de aynı şeyi yaptığını varsayıyorum.
BЈовић

5
Demek istediğim bu değil. Değerle veya referansla geçmenin daha iyi olup olmadığı, işlevin içinde ne yaptığınıza bağlıdır. Referanstır böylece örnekte, aslında dize nesnenin çok kullanmadığınız açıkçası daha iyi. Ancak, işlevin görevi dizeyi bazı yapılara yerleştirmek veya dize birden çok bölmesini içeren bazı özyinelemeli algoritma gerçekleştirmekse, değere göre geçmek referans ile geçirmeye kıyasla aslında bazı kopyaları kaydedebilir . Nicol Bolas bunu gayet iyi açıklıyor.
leftaroundabout

3
Bana göre "bu işlev içinde ne yaptığınıza bağlıdır" kötü tasarım - çünkü işlevin imzasını uygulamanın içlerine dayandırıyorsunuz.
Hans Olsson

1
Bir yazım hatası olabilir, ancak son iki gerçek zamanlamanın 10 kat daha az döngüsü vardır.
TankorSmash

54

Kısa cevap: HAYIR! Uzun cevap:

  • Dizeyi değiştirmezseniz (tedavi salt okunurdur), olarak iletin const ref&.
    ( const ref&Açıkçası onu kullanan işlev yürütülürken kapsam dahilinde kalmalıdır)
  • Değiştirmeyi planlıyorsanız veya kapsam dışına çıkacağını biliyorsanız (iş parçacıkları) , a olarak iletin , işlev gövdesinin içini valuekopyalamayın const ref&.

Üzerinde bir post vardı cpp-next.com denilen "istiyor hız değeriyle geçmesi," . TL; DR:

Yönerge : İşlev argümanlarınızı kopyalamayın. Bunun yerine, bunları değere göre iletin ve derleyicinin kopyalamayı yapmasına izin verin.

ÇEVİRİ ^

İşlev argümanlarınızı kopyalamayın --- şu anlama gelir: bağımsız değişken değerini dahili bir değişkene kopyalayarak değiştirmeyi planlıyorsanız, bunun yerine bir değer bağımsız değişkeni kullanın .

Yani, bunu yapma :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

bunu yap :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

İşlev gövdesindeki bağımsız değişken değerini değiştirmeniz gerektiğinde.

Sadece bağımsız değişkeni işlev gövdesinde nasıl kullanmayı planladığınızın farkında olmanız gerekir. Salt okunur veya DEĞİL ... ve kapsama alanı içinde kalırsa.


2
Bazı durumlarda referansla geçmenizi öneririz, ancak her zaman değere göre geçiş yapılmasını öneren bir yönerge işaret edersiniz.
Keith Thompson

3
@KeithThompson İşlev argümanlarınızı kopyalama. Anlamına const ref&gelir değiştirmek için bir iç değişken kopyalamayın . Değiştirmeniz gerekiyorsa ... parametreyi bir değer yapın. İngilizce bilmeyen kendim için oldukça açık.
CodeAngry

5
@KeithThompson Kılavuz alıntı (İşlev argümanlarınızı kopyalamayın. Bunun yerine, bunları değere göre iletin ve derleyicinin kopyalamayı yapmasına izin verin.) Bu sayfadan KOPYALIDIR. Eğer bu yeterince açık değilse, yardım edemem. En iyi seçimleri yapmak için derleyicilere tam olarak güvenmiyorum. İşlev argümanlarını tanımlama biçimimde niyetlerim konusunda çok net olmayı tercih ederim. # 1 Salt okunursa, bir const ref&. # 2 Yazmam gerekiyorsa veya kapsam dışında kaldığını biliyorum ... Bir değer kullanıyorum. # 3 Orijinal değeri değiştirmem gerekirse, geçerim ref&. # 4 pointers *Bir argüman isteğe bağlıysa kullanırım nullptr.
CodeAngry

11
Değerle mi, yoksa referansla mı geçileceği sorusunda taraf tutmuyorum. Demek istediğim, bazı durumlarda referansla geçmeyi savunuyorsunuz, ama sonra (görünüşte pozisyonunuzu destekliyor gibi) her zaman değerden geçmeyi öneren bir rehberden bahsediyorsunuz. Yönergeye katılmıyorsanız, bunu söylemek ve nedenini açıklamak isteyebilirsiniz. (Cpp-next.com bağlantıları benim için çalışmıyor.)
Keith Thompson

4
@ KeithThompson: Kılavuzu yanlış yorumluyorsun. Değeri "her zaman" geçmek değildir. Özetlemek gerekirse, "Yerel bir kopya çıkarırsanız, derleyicinin bu kopyayı sizin için gerçekleştirmesini sağlamak için pass by value kullanın." Bir kopya oluşturmayacağınız zaman, her işe uygun değeri kullanmak demek değildir.
Ben Voigt

43

Aslında bir kopyaya ihtiyacınız olmadığı sürece çekilmesi makul olur const &. Örneğin:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

Dizeyi değere göre alacak şekilde değiştirirseniz, parametreyi taşıyabilir veya kopyalayabilirsiniz ve buna gerek yoktur. Kopyalama / taşıma sadece daha pahalı olmakla kalmaz, aynı zamanda yeni bir potansiyel arıza da getirir; kopyalama / taşıma bir istisna fırlatabilir (örn. kopyalama sırasında ayırma başarısız olabilir), ancak mevcut bir değere referans almak mümkün değildir.

Eğer varsa do kopyası gerekiyor sonra geçen ve değeri ile dönen genellikle (her zaman?) En iyi seçenek. Aslında, ekstra kopyaların aslında bir performans sorununa neden olduğunu bulamazsanız genellikle C ++ 03'te endişelenmeyeceğim. Kopya seçimleri modern derleyicilerde oldukça güvenilir görünüyor. İnsanların RVO için derleyici desteği tablonuzu kontrol etmeniz gerektiğine dair şüpheleri ve ısrarları, günümüzde çoğunlukla eski.


Kısacası, C ++ 11, kopya seçimine güvenmeyen insanlar dışında bu konuda hiçbir şeyi gerçekten değiştirmez.


2
Move yapıcılar genellikle ile uygulanır noexcept, ancak kopya yapıcılar açık değildir.
leftaroundabout

25

Neredeyse.

C ++ 17'de, parametreler basic_string_view<?>için bizi temel olarak bir dar kullanım durumuna std::string const&indirir.

Hareket semantiğinin varlığı, bir kullanım örneğini ortadan kaldırmıştır std::string const&- eğer parametreyi saklamayı planlıyorsanız, parametrenin dışında std::stringolabildiğince bir by değeri almak daha uygundur move.

Birisi ham C ile fonksiyonunuzu çağırdıysa, bu durumda iki "string"tanesinin std::stringaksine sadece bir tampon tahsis edilir std::string const&.

Ancak, bir kopya yapmak istemiyorsanız std::string const&, C ++ 14'te yine de yararlıdır.

Bununla birlikte std::string_view, söz konusu dizeyi C stili '\0'sonlandırılmış karakter arabellekleri bekleyen bir API'ye geçirmediğiniz sürece, std::stringherhangi bir ayırma riski olmadan işlevsellik gibi daha verimli bir şekilde alabilirsiniz . İşlenmemiş bir C dizesi, std::string_viewherhangi bir ayırma veya karakter kopyalama olmadan bir 'e dönüştürülebilir .

Bu noktada, std::string const&veri toptan satışını kopyalamadığınızda ve boş bir sonlandırılmış arabellek bekleyen bir C stili API'ye geçireceğiniz ve bunun için daha yüksek düzeyli dize işlevlerine ihtiyacınız olacak std::string. Uygulamada, bu nadir bir gereksinim kümesidir.


2
Bu cevabı takdir ediyorum - ancak alan adına özgü bir yanlılıktan dolayı (çok sayıda kaliteli cevap olduğu gibi) acı çektiğini belirtmek istiyorum. Şunu belirtmek gerekirse: “Uygulamada, bu nadir bir gereklilikler kümesidir”… kendi gelişim tecrübelerime göre, yazar için anormal derecede dar görünen bu kısıtlamalar, kelimenin tam anlamıyla her zaman karşılanıyor. Bunu belirtmeye değer.
fish2000

1
@ fish2000 Açık olmak gerekirse, std::stringegemen olmak için sadece bu gereksinimlerin bazılarına değil, hepsine de ihtiyacınız var . Bunlardan herhangi biri, hatta ikisi yaygındır. Belki de genel olarak 3'e ihtiyacınız vardır (örneğin, hangi C API'sını toptan satışa geçireceğinizi seçmek için bir dize argümanını biraz ayrıştırıyorsunuz?)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont Bu bir YMMV olayıdır - ancak POSIX'e veya C-string anlambiliminin en düşük ortak payda olduğu diğer API'lara karşı programlıyorsanız sık kullanılan bir durumdur. Gerçekten sevdiğimi söylemeliyim std::string_view- işaret ettiğiniz gibi, "Ham C dizesi bile herhangi bir tahsis veya karakter kopyalama olmadan bir std :: string_view dönüştürülebilir" ki bu bağlamda C ++ kullananlara hatırlamaya değer bir şey gerçekten böyle bir API kullanımı.
fish2000

1
@ fish2000 "'Ham C dizesi, herhangi bir ayırma veya karakter kopyalama olmadan std :: string_view biçimine dönüştürülebilir' ki bu da hatırlanmaya değer bir şeydir". Gerçekten de, en iyi kısmı çıkarır - ham dizenin bir dize değişmez olması durumunda, bir çalışma zamanı strlen () bile gerektirmez !
Don Hatch

17

std::stringdeğil Düz Eski Verileri (POD) ve ham boyutu şimdiye kadarki en alakalı bir şey değildir. Örneğin, TOA uzunluğunun üzerinde olan ve öbek üzerinde ayrılan bir dize iletirseniz, kopya oluşturucunun SSO depolamasını kopyalamamasını beklerim.

Bunun önerilmesinin nedeni inval, argüman ifadesinden yapılmış olması ve bu nedenle her zaman uygun şekilde taşınması veya kopyalanmasıdır - argümanın sahipliğine ihtiyacınız olduğunu varsayarsak performans kaybı olmaz. Bunu yapmazsanız, constbaşvuru yine de daha iyi bir yol olabilir.


2
Kopya yapıcısı, kullanmadığı takdirde TOA hakkında endişelenmeyecek kadar akıllı olmasıyla ilgili ilginç bir nokta. Muhtemelen doğru, bunun doğru olup olmadığını kontrol etmek zorundayım ;-)
Benj

3
@ Benj: Eski yorum biliyorum, ama TOA koşulsuz olarak kopyalamak için yeterince küçükse koşullu bir dal yapmaktan daha hızlıdır. Örneğin, 64 bayt bir önbellek hattıdır ve gerçekten önemsiz bir sürede kopyalanabilir. Muhtemelen x86_64'te 8 döngü veya daha az.
Zan Lynx

TOA, kopya oluşturucu tarafından kopyalanmasa bile std::string<>, yığından ayrılan 32 bayttır ve 16'sı başlatılmalıdır. Bunu, bir başvuru için ayrılan ve başlatılan yalnızca 8 baytla karşılaştırın: CPU işinin iki katıdır ve diğer veriler için kullanılamayacak dört kat daha fazla önbellek alanı kaplar.
cmaster - eski haline monica

Oh, ve yazmaçlarda fonksiyon argümanlarını aktarmayı konuşmayı unuttum; bu, son arayan için referansın yığın kullanımını sıfıra
indirir

16

Bu sorunun cevabını buraya kopyaladım / yapıştırdım ve isimleri ve yazımı bu soruya uyacak şekilde değiştirdim.

Sorulan soruları ölçmek için kod:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Benim için bu çıktılar:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

Aşağıdaki tablo sonuçlarımı özetler (clang -std = c ++ 11 kullanarak). İlk sayı kopya yapı sayısı ve ikinci sayı taşıma yapı sayısıdır:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

Geçiş değeri çözümü yalnızca bir aşırı yük gerektirir, ancak değer ve x değerlerini geçerken ek bir hareket yapısına mal olur. Bu, herhangi bir durum için kabul edilebilir veya olmayabilir. Her iki çözümün de avantajları ve dezavantajları vardır.


1
std :: string standart bir kütüphane sınıfıdır. Zaten hem taşınabilir hem de kopyalanabilir. Bunun ne kadar alakalı olduğunu anlamıyorum. OP, hamle ile kopyanın performansı hakkında değil, hamle ile referansların performansı hakkında daha fazla soru soruyor .
Nicol Bolas

3
Bu cevap, bir std :: stringinin Herb ve Dave tarafından tarif edilen ve her iki aşırı yük fonksiyonuyla referans olarak geçilen değer bazında tasarımına tabi tutulacak hamle ve kopya sayısını sayar. Ben kopyalanır / taşınırken bağırmak için kukla bir dize yerine haricinde, demo OP kodunu kullanın.
Howard Hinnant

Muhtemelen testleri yapmadan önce kodu optimize etmelisiniz…
Paramanyetik Kruvasan

3
@TheParamagneticCroissant: Farklı sonuçlar aldınız mı? Öyleyse, hangi derleyiciyi hangi komut satırı argümanlarıyla kullanmak?
Howard Hinnant

14

Herb Sutter, const std::string&parametre tipi olarak tavsiye edilirken , Bjarne Stroustroup ile birlikte hala kayıt altındadır ; görmek https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

Buradaki diğer cevapların hiçbirinde belirtilmeyen bir tuzak vardır: bir const std::string&parametreye değişmez bir dize iletirseniz , değişmez karakterleri tutmak için anında oluşturulan geçici bir dizeye bir başvuru iletir. Daha sonra bu başvuruyu kaydederseniz, geçici dize ayrıldıktan sonra geçersiz olur. Güvende olmak için referansı değil bir kopyasını kaydetmelisiniz . Sorun, dizgi değişmezlerinin const char[N]terfi gerektiren türler olmasından kaynaklanmaktadır std::string.

Aşağıdaki kod, küçük bir verimlilik seçeneğiyle birlikte tuzak ve geçici çözümü gösterir - bir const char*yöntemle aşırı yükleme , , C ++ 'da referans olarak bir dizgi hazır bilgisini geçirmenin bir yolu var mı ?

(Not: Sutter & Stroustroup, dizenin bir kopyasını saklıyorsanız, && parametresi ve std :: move () ile aşırı yüklenmiş bir işlev sağlamanızı önerir.)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

ÇIKTI:

const char * constructor
const std::string& constructor

Second string
Second string

Bu farklı bir sorun ve WidgetBadRef yanlış gitmek için bir const & parametre olması gerekmez. Soru WidgetSafeCopy daha yavaş olurdu bir string parametresi aldı mı? (Üyenin geçici kopyasının tespit edilmesi kesinlikle daha kolay)
Superfly Jon

T&&Her zaman evrensel bir referans olmadığını unutmayın ; aslında, std::string&&her zaman bir değer referansı ve asla evrensel bir referans olacaktır, çünkü hiçbir tür kesinti yapılmaz . Böylece, Stroustroup & Sutter'in tavsiyesi Meyers'le çelişmez.
Justin Time - Monica'yı

@ JustinTime: teşekkür ederim; Aslında, std :: string && evrensel bir referans olacağını iddia eden yanlış son cümleyi kaldırdım.
circlepi314

@ circlepi314 Rica ederim. Yapılması kolay bir karışımdır, bazen verilen herhangi T&&bir çıkarımın evrensel bir referans mı yoksa çıkarılmamış bir rvalue referansı mı olduğu kafa karıştırıcı olabilir ; onlar (örneğin evrensel referanslar için farklı bir sembol tanıtıldı eğer muhtemelen daha net olurdu &&&bir kombinasyonu olarak, &ve &&), ama muhtemelen sadece aptalca görünürdü.
Justin Time - Monica'yı

8

C ++ referansını kullanan IMO std::stringhızlı ve kısa bir yerel optimizasyondur, değerden geçerken kullanmak daha iyi bir global optimizasyon olabilir (veya olmayabilir).

Cevap: koşullara bağlı:

  1. Tüm kodu dışarıdan iç fonksiyonlara yazarsanız, kodun ne yaptığını bilirsiniz, referansı kullanabilirsiniz const std::string &.
  2. Kütüphane kodunu yazarsanız veya dizelerin geçtiği yerlerde yoğun kütüphane kodu kullanırsanız, std::stringkopya oluşturucu davranışına güvenerek küresel anlamda daha fazla kazanç elde edersiniz .

6

Bkz Modern C ++ Stil Temelleri! Essentials “Herb Sutter "Geri” . Diğer konular arasında, o C ++ 11 ile gelir ve spesifik olarak bakar, geçmişte verilmiş bulunuyor parametre aktararak, öneri ve yeni fikirler gözden dizeleri değere göre geçirme fikri.

24. slayt

Karşılaştırmalar std::string, işlevin yine de kopyalayacağı durumlarda, değere göre geçmenin önemli ölçüde daha yavaş olabileceğini göstermektedir!

Bunun nedeni, const&sürümü her zaman tam bir kopya oluşturmaya zorlamanızdır (ve daha sonra yerine taşıyın), sürüm zaten tahsis edilen arabelleği yeniden kullanabilen eski dizeyi güncelleyecektir.

Bkz. Slaydı 27: “Ayar” işlevleri için seçenek 1 her zamanki gibi aynıdır. Seçenek 2, rvalue referansı için aşırı yük ekler, ancak bu, birden fazla parametre varsa birleşik bir patlama sağlar.

Yalnızca, bir dizenin oluşturulması gereken (mevcut değerinin değiştirilmemesi gereken) “batma” parametreleri için, değere göre geçiş numarasının geçerli olması gerekir. Yani, inşaatçılar parametrenin doğrudan eşleşen türün üyesini başlattığı .

Bu konuda endişelenmeye ne kadar derin gidebileceğinizi görmek istiyorsanız, Nicolai Josuttis'in sunumunu ve bununla iyi şanslar izleyin ( önceki sürümde hata bulduktan sonra n kez “Mükemmel - Bitti!” . Hiç orada mıydınız ?)


Bu ayrıca Standart Kılavuzlarda inF.15 olarak özetlenmiştir .


3

@ JDługosz'un yorumlarda belirttiği gibi, Herb başka bir konuşmada (daha sonra?) Başka tavsiyeler verir, kabaca buradan bakın: https://youtu.be/xnqTKD8uD64?t=54m50s .

Onun tavsiyesi f, yapıyı bu havuz argümanlarından taşıyacağınızı varsayarak, yalnızca havuz argümanları alan bir fonksiyon için değer parametrelerini kullanmaya dayanır.

Bu genel yaklaşım, fsırasıyla lvalue ve rvalue argümanlarına göre uyarlanmış optimal bir uygulama ile karşılaştırıldığında, hem lvalue hem de rvalue argümanları için bir hareket yapıcısının ek yükünü ekler . Bunun neden böyle olduğunu görmek için , bir copy fparametresi aldığını varsayalım , burada Tbazı kopyalama ve taşıma yapıcı türü:

void f(T x) {
  T y{std::move(x)};
}

çağrı fBir değer bağımsız değişkeni ile çağrıldığında, bir kopya oluşturucunun kurulax ve bir hareket kurucusunun da kurmaya sonuçlanır y. Öte yandan, fbir rvalue argümanı ile çağrılmak bir hareket kurucusunun inşa edilmesine xve başka bir hareket kurucusunun inşaa çağrılmasına neden olury .

Genel olarak, flvalue argümanlarının optimum uygulaması aşağıdaki gibidir:

void f(const T& x) {
  T y{x};
}

Bu durumda, yalnızca bir kopya oluşturucu oluşturmaya çağrılır. y . fDeğerleme argümanlarının optimal uygulaması yine genel olarak şu şekildedir:

void f(T&& x) {
  T y{std::move(x)};
}

Bu durumda, yalnızca bir hamle yapıcısı y .

Bu nedenle mantıklı bir uzlaşma, bir değer parametresi almak ve optimal uygulama ile ilgili lvalue veya rvalue argümanları için ekstra bir hareket oluşturucu çağrısı yapmaktır, bu da Herb'in konuşmasında verilen tavsiyedir.

@ JDługosz'un yorumlarda belirttiği gibi, değere göre geçmek sadece lavabo argümanından bir nesne oluşturacak fonksiyonlar için anlamlıdır. fBağımsız değişkenini kopyalayan bir işleve sahip olduğunuzda, değere göre by-pass yaklaşımı, genel-by-const-referans yaklaşımından daha fazla ek yüke sahip olacaktır. fParametresinin bir kopyasını saklayan bir fonksiyonun değer bazında yaklaşımı şu şekle sahip olacaktır:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

Bu durumda, bir kopya yapısı ve bir değer bağımsız değişkeni için bir taşıma ataması ve bir değer bağımsız değişkeni için bir taşıma yapısı ve taşıma ataması vardır. Lvalue argümanı için en uygun durum:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

Bu, yalnızca kopya oluşturucudan artı değer-değeri yaklaşımı için gereken taşıma atamasından çok daha ucuz olan bir atamaya kadar kaynar. Bunun nedeni, atamanın mevcut ayrılan belleği yeniden kullanabilmesidir.y ve bu nedenle (de) ayırmayı önleyebilmesidir, oysa kopya oluşturucu genellikle belleği tahsis edecektir.

Bir rvalue argümanı için f, bir kopyayı muhafaza eden en uygun uygulama şu şekildedir:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Yani, bu durumda sadece bir hareket ödevi. fBir const başvurusu alan sürümüne bir rvalue iletmek , bir taşıma ataması yerine yalnızca bir atamaya mal olur. Yani nispeten konuşursak,f bu durumda genel uygulama olarak bir const referansı alma tercih edilir.

Bu nedenle, genel olarak, en uygun uygulama için, konuşmada gösterildiği gibi aşırı yükleme veya bir tür mükemmel yönlendirme yapmanız gerekir. Dezavantajı, fargümanın değer kategorisinde aşırı yüklemeyi seçmeniz durumunda parametre sayısına bağlı olarak, gerekli aşırı yük sayısında bir kombinasyon patlamasıdır . Mükemmel yönlendirme, fsanallaştırmayı önleyen bir şablon işlevi haline gelen dezavantaja sahiptir ve % 100 doğru elde etmek istiyorsanız önemli ölçüde daha karmaşık bir kodla sonuçlanır (kanlı ayrıntılar için konuşmaya bakın).


Herb Sutter'ın yeni cevabımdaki bulgularına bakın: bunu sadece yapıyı hareket ettirdiğinizde yapın, atamayı hareket ettirmeyin.
JDługosz

1
@ JDługosz, Herb'in konuşmasına işaretçi için teşekkürler, sadece izledim ve cevabımı tamamen revize ettim. (Hareket) atama tavsiyesinin farkında değildim.
Ton van den Heuvel


1

Sorun "const" taneli olmayan bir niteleyici olmasıdır. Genellikle "const string ref" ile kastedilen "referans sayısını değiştirmeyin" değil, "bu dizeyi değiştirmeyin" dir. C ++ 'ta hangi üyelerin "const" olduğunu söylemenin hiçbir yolu yoktur . Hepsi ya öyle ya da hiçbiri değil.

Bu dil sorununu etrafında kesmek için, STL olabilir "C ()" seçeneğini örnekte bir hareket-semantik kopyasını izin zaten başvuru sayısı (değişebilir) bakımından "const" görmezden aldatılan ve. İyi tanımlandığı sürece, bu iyi olurdu.

STL olmadığından, const_casts <> referans sayacını uzatan bir dize sürümüne sahibim (geriye dönük olarak bir sınıf hiyerarşisinde değiştirilebilir bir şey yapmanın yolu yok) ve - lo ve işte - cmstring'in const başvuruları olarak serbestçe geçebileceğini, ve tüm gün boyunca sızıntı veya sorun yaşamadan derin işlevlerde kopyalarını alın.

C ++ burada "türetilmiş sınıf const granülerliği" sunmadığından, iyi bir şartname yazmak ve parlak yeni bir "const hareketli dize" (cmstring) nesnesi yapmak gördüğüm en iyi çözümdür.


@BenVoigt yep ... uzaklaştırmak yerine, değişebilir olmalı ... ama bir STL üyesini türetilmiş bir sınıfta değişebilir olarak değiştiremezsiniz.
Erik Aronesty
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.