Değere göre geçirme ve std :: başvuruya göre aktarmanın avantajları


109

Şu anda C ++ öğreniyorum ve kötü alışkanlıklardan kaçınmaya çalışıyorum. Anladığım kadarıyla, clang-tidy birçok "en iyi uygulama" içeriyor ve bunlara mümkün olduğunca en iyi şekilde bağlı kalmaya çalışıyorum ( bunların neden iyi kabul edildiğini tam olarak anlamamama rağmen), ancak emin değilim burada neyin önerildiğini anlayın.

Bu dersi öğreticiden kullandım:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

Bu, clang-tidy'den referans ve kullanım yerine değere göre geçmem gerektiği önerisine yol açar std::move. Ben bunu yaparsak, öneri yapmak olsun nameve uyarıyı (her zaman kopyalanan almaz sağlamak için) bir başvuru std::movenedeniyle herhangi bir etkisi olmaz namebir olduğunu constbunu kaldırmalısınız bu yüzden.

Bir uyarı constalmamamın tek yolu tamamen kaldırmaktır :

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Bu mantıklı görünüyor, çünkü tek faydası constorijinal dizeyle karışıklığı önlemekti (bu, değere göre geçtiğim için olmuyor). Ama CPlusPlus.com'da okudum :

Her ne kadar standart kitaplıkta taşıma işleminin taşınan nesnenin geçerli ancak belirtilmemiş bir durumda kaldığı anlamına geldiğini unutmayın. Bu, böyle bir işlemden sonra, taşınan nesnenin değerinin yalnızca yok edilmesi veya yeni bir değer atanması gerektiği anlamına gelir; aksi takdirde ona erişmek, belirtilmemiş bir değer verir.

Şimdi şu kodu hayal edin:

std::string nameString("Alex");
Creature c(nameString);

Çünkü nameStringdeğeriyle geçirilen, std::moveancak geçersiz kılar namekurucu içinde ve orijinal dize dokunmayın. Ama bunun avantajları nelerdir? Görünüşe göre içerik her nasılsa yalnızca bir kez kopyalanacak - aradığımda referans olarak geçersem, geçtiğimde m_name{name}değere göre geçersem (ve sonra taşınır). Bunun değere göre geçmekten ve kullanmamaktan daha iyi olduğunu anlıyorum std::move(çünkü iki kez kopyalanır).

Yani iki soru:

  1. Burada ne olduğunu doğru anladım mı?
  2. std::moveReferansla geçmeyi kullanıp sadece çağırmanın herhangi bir avantajı var mı m_name{name}?

3
Referansla geçerek, Creature c("John");fazladan bir kopya oluşturur
user253751

1
Bu bağlantı değerli bir okuma olabilir, geçiş std::string_viewve SSO'yu da kapsar .
lubgr

Bulduğum clang-tidyokunabilirlik pahasına gereksiz microoptimisations Saplantı kendim almak için harika bir yol olduğunu. Sorusu her şeyden önce, biz kaç kere burada, sormak aslında çağrı Creatureyapıcı.
cz

Yanıtlar:


39
  1. Burada ne olduğunu doğru anladım mı?

Evet.

  1. Kullanmanın herhangi bir avantajı var mı std::moveReferansla geçmeyi sadece çağırmanın m_name{name}?

Herhangi bir ek aşırı yükleme olmaksızın, anlaşılması kolay işlev imzası. İmza, argümanın kopyalanacağını hemen ortaya çıkarır - bu, arayanlarınconst std::string& referansın bir veri üyesi olarak saklanıp saklanmayacağını , muhtemelen daha sonra sarkan bir referans haline gelir. Ve fonksiyona rdeğerler iletildiğinde gereksiz kopyalardan kaçınmak için aşırı yüklemeye std::string&& nameve const std::string&bağımsız değişkenlere gerek yoktur . Bir ldeğer geçmek

std::string nameString("Alex");
Creature c(nameString);

argümanını değere göre alan işleve bir kopya ve bir hareket yapısına neden olur. Aynı işleve bir rvalue geçirmek

std::string nameString("Alex");
Creature c(std::move(nameString));

iki hareket yapısına neden olur. Bunun tersine, işlev parametresi olduğunda const std::string&, bir rvalue bağımsız değişkenini iletirken bile her zaman bir kopya olacaktır. Bu, argüman türü taşımak-oluşturmak için ucuz olduğu sürece açıkça bir avantajdır (durum budur std::string).

Ancak dikkate alınması gereken bir dezavantaj vardır: mantık, işlev bağımsız değişkenini başka bir değişkene atayan işlevler için çalışmaz (onu başlatmak yerine):

void setName(std::string name)
{
    m_name = std::move(name);
}

m_nameyeniden atanmadan önce başvuruda bulunan kaynağın serbest bırakılmasına neden olur . Etkili Modern C ++ 'daki Madde 41'i ve ayrıca bu soruyu okumanızı tavsiye ederim .


Bu mantıklıdır, özellikle bu beyanı okumayı daha sezgisel hale getirir. Cevabınızın serbest bırakma kısmını tam olarak anladığımdan (ve bağlantılı iş parçacığını anladığımdan) emin değilim, bu yüzden sadece kontrol etmek için kullanırsam move, alan serbest bırakılır . Ben kullanmazsanız moveayrılan uzay geliştirilmiş performans yol açan yeni dize tutmak için çok küçük olması durumunda, sadece bırakılmaktadır. Bu doğru mu?
Blackbot

1
Evet, aynen öyle. İçin atarken m_namebir gelen const std::string&parametre, dahili bellek olduğu sürece kullanılan yeniden olduğunu m_namesığmayacak. Hareket-atarken m_name, bellek önceden ayırmanın gerekir. Aksi takdirde, görevin sağ tarafından kaynakları "çalmak" imkansızdı.
lubgr

Ne zaman sarkan bir referans haline gelir? Sanırım başlatma listesi derin kopya kullanıyor.
Li Taiji

109
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • A geçti lvalue bağlanmadığını nameardından edilir kopyalanan içine m_name.

  • A geçti rvalue bağlanmadığını nameardından edilir kopyalanan içine m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Bir geçirilen lvalue edilir kopyalanan içine namedaha sonra edilir taşındı içine m_name.

  • Bir geçirilir rvalue olan hareket içine namedaha sonra bir, hareket halinde m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • A geçti lvalue bağlanmadığını nameardından edilir kopyalanan içine m_name.

  • Bir geçen rvalue bağlanmadığını rname, daha sonra bir hareket halinde m_name.


Taşıma işlemleri genellikle kopyalardan daha hızlı olduğundan, çok sayıda geçiciyi geçerseniz (1) (0) ' dan daha iyidir . (2) kopyalar / hareketler açısından idealdir ancak kod tekrarını gerektirir.

Mükemmel yönlendirme ile kod tekrarından kaçınılabilir :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

Bu Tyapıcının örneklenebileceği türlerin etki alanını kısıtlamak için isteğe bağlı olarak kısıtlamak isteyebilirsiniz (yukarıda gösterildiği gibi). C ++ 20, bunu Concepts ile basitleştirmeyi amaçlamaktadır .


C ++ 17'de, pr değerler garantili kopya seçiminden etkilenir bu, uygulanabilir olduğunda, bağımsız değişkenleri işlevlere kopya / hareket sayısını azaltır.


(1) için pr-değeri ve xvalue durumu c ++ 17 hayır?
Oliv

1
Bu durumda mükemmel ilerlemek için SFINAE'ye ihtiyacınız olmadığını unutmayın . Sadece belirsizliği gidermek için gerekli. O var akla yatkın kötü argümanlar geçerken potansiyel hata mesajları için yararlı
Caleth

@Oliv Evet. xvalues ​​taşınmalı, prvalues ​​elden çıkarılabilir :)
Rakete1111

1
Biz yazabilir: Creature(const std::string &name) : m_name{std::move(name)} { }in (2) ?
skytree

5
@skytree: Bir const nesnesinden hareket edemezsiniz çünkü hareket etmek kaynağı değiştirir. Bu derlenecek, ancak bir kopyasını oluşturacaktır.
Vittorio Romeo

1

Nasıl geçmek burada tek değişken değildir, neyi geçmek ikisi arasında büyük bir fark yapar.

C ++, elimizdeki değeri kategorilerinin her türlü ve bu "deyim" Bir geçmek durumlar için mevcut rvalue (gibi "Alex-string-literal-that-constructs-temporary-std::string"ya std::move(nameString)sonuçlanır), 0 kopya ait std::stringkopyalamaya karşı constructible olmak için yapılıyor (tip bile yok rvalue argümanları için) ve yalnızca std::string's move yapıcısını kullanır .

Bir şekilde ilgili Soru-Cevap .


1

Geçiş-by-(rv) referansına göre değerle geçiş ve hareket ettirme yaklaşımının birkaç dezavantajı vardır:

  • 2 yerine 3 nesnenin ortaya çıkmasına neden olur;
  • bir nesneyi değere göre geçirmek, fazladan yığın ek yüküne yol açabilir, çünkü normal dizgi sınıfı bile tipik olarak bir göstericiden en az 3 veya 4 kat daha büyüktür;
  • argüman nesnelerinin oluşturulması, arayan tarafında kod şişmesine neden olacak;

Neden 3 nesnenin ortaya çıkmasına neden olacağını açıklayabilir misiniz? Anladığım kadarıyla "Peter" ı bir dizge olarak geçirebilirim. Bu ortaya çıkar, kopyalanır ve sonra taşınır, değil mi? Ve yığın ne olursa olsun bir noktada kullanılmaz mı? Yapıcı çağrısı noktasında değil, m_name{name}kopyalandığı kısımda mı?
Blackbot

@Blackbot Örneğinizden bahsediyordum, std::string nameString("Alex"); Creature c(nameString);bir nesne nameString, diğeri işlev bağımsız değişkeni ve üçüncüsü bir sınıf alanı.
user7860670

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.