C ++ 'da bir nesne döndürmek nasıl?


167

Birçok benzer soru var gibi başlık tanıdık geliyor biliyorum, ama sorunun farklı bir yönünü soruyorum (yığın üzerinde şeyler var ve yığın üzerine koymak arasındaki farkı biliyorum).

Java'da her zaman "yerel" nesnelere başvuruları döndürebilirim

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

C ++, benzer bir şey yapmak için 2 seçenek var

(1) Bir nesneyi "döndürmem" gerektiğinde referansları kullanabilirim

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

O zaman böyle kullan

Thing thing;
calculateThing(thing);

(2) Veya dinamik olarak ayrılmış bir nesneye bir işaretçi döndürebilirim

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

O zaman böyle kullan

Thing* thing = calculateThing();
delete thing;

İlk yaklaşımı kullanarak hafızayı elle boşaltmak zorunda kalmayacağım, ama bana göre kodun okunmasını zorlaştırıyor. İkinci yaklaşımla ilgili sorun, hatırlamak zorunda kalacağım delete thing;, ki bu hoş görünmüyor. Kopyalanan bir değeri döndürmek istemiyorum çünkü verimsiz (sanırım), işte sorular geliyor

  • Üçüncü bir çözüm var mı (değerin kopyalanmasını gerektirmez)?
  • İlk çözüme bağlı kalırsam herhangi bir sorun var mı?
  • İkinci çözümü ne zaman ve neden kullanmalıyım?

32
Soruyu güzelce ifade etmek için +1.
Kangkan

1
Çok bilgiç olmak için, "işlevlerin bir şey döndürdüğünü" söylemek biraz kesin değildir. Daha doğrusu, bir işlev çağrısını değerlendirmek bir değer üretir . Değer her zaman bir nesnedir (geçersiz bir işlev olmadığı sürece). Ayrım, değerin bir beyan veya ön değer olup olmadığıdır - beyan edilen dönüş türünün bir referans olup olmadığı ile belirlenir .
Kerrek SB

Yanıtlar:


107

Kopyalanan bir değeri döndürmek istemiyorum çünkü verimsiz

Kanıtla.

RVO ve NRVO ve C ++ 0x hareket semantiğine bakın. Çoğu durumda, C ++ 03'te bir out parametresi kodunuzu çirkin hale getirmenin iyi bir yoludur ve C ++ 0x'de bir out parametresi kullanarak kendinizi incitirsiniz.

Sadece temiz kod yazın, değere göre döndürün. Performans bir sorunsa profil oluşturun (tahmin etmeyi durdurun) ve düzeltmek için neler yapabileceğinizi bulun. Muhtemelen bir şeyleri işlevlerden döndürmeyecektir.


Bununla birlikte, eğer böyle yazmaya hazırsanız, muhtemelen out parametresini yapmak istersiniz. Daha güvenli ve genellikle daha hızlı olan dinamik bellek ayırmayı önler. İşlevi çağırmadan önce nesneyi oluşturmak için bir yolunuz olmasını gerektirir, bu da tüm nesneler için her zaman anlamlı değildir.

Dinamik ayırmayı kullanmak istiyorsanız, yapılabilecek en az şey bunu akıllı bir işaretçiye koymaktır. (Bu her zaman her zaman yapılmalıdır) O zaman bir şey silme konusunda endişelenmeyin, işler istisna güvenlidir, vb. Tek sorun zaten değerle döndürmekten daha yavaş olması!


10
@phunehehe: Hiçbir nokta spekülasyon yapmıyor, kodunuzu profille oluşturmalı ve öğrenmelisiniz. (İpucu: hayır.) Derleyiciler çok akıllıdır, gerekmedikçe işleri kopyalamak için zaman kaybetmezler. Kopyalamanın bir maliyeti olsa bile , yine de hızlı kod üzerinden iyi bir kod için çaba göstermelisiniz; iyi bir kod hız sorunu olduğunda optimize etmek kolaydır. Hiçbir fikriniz olmayan bir şey için kod çirkin bir anlamı yok bir sorun; özellikle de yavaşlatırsanız veya hiçbir şey almazsanız. Ve C ++ 0x kullanıyorsanız, hareket semantiği bunu bir sorun değildir.
GManNickG

1
@GMan, re: RVO: aslında bu sadece arayanınız ve callee'niz aynı derleme birimindeyse doğrudur, ki bu gerçek dünyada çoğu zaman değildir. Bu nedenle, kodunuzun tamamı ayarlanmamışsa (bu durumda hepsi bir derleme biriminde olacaksa) veya bazı bağlantı zamanı optimizasyonunuz varsa (GCC'de yalnızca 4.5'ten itibaren) hayal kırıklığına uğradınız.
Alex B

2
@Alex: Derleyiciler çeviri birimleri arasında optimizasyon konusunda gittikçe daha iyi hale geliyor. (VC şimdi birkaç sürüm için yapıyor.)
sbi

9
@Alex B: Bu tam bir çöp. Pek çok yaygın çağrı kuralı, arayanı büyük geri dönüş değerleri için alan ayırmaktan sorumlu kılar. RVO, bağlantı zamanı optimizasyonları olmasa bile derleme birimlerinde mutlu bir şekilde çalışır.
CB Bailey

6
@Charles, kontrol ettikten sonra doğru gibi görünüyor! Açıkça yanlış bilgilendirilmiş ifademi geri çekiyorum.
Alex B

41

Sadece nesneyi oluşturun ve geri gönderin

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Optimizasyonu unutursanız ve sadece okunabilir kod yazarsanız kendinize bir iyilik yapacağınızı düşünüyorum (daha sonra bir profiler çalıştırmanız gerekir - ancak önceden optimize etmeyin).


2
Thing thing();yerel bir işlev bildirir ve geri döner Thing.
dreamlax

2
Şey (), bir şey döndüren bir işlevi bildirir. İşlev gövdenizde oluşturulmuş bir Şey nesnesi yok.
CB Bailey

@dreamlax @Charles @GMan Biraz geç ama düzeltildi.
Amir Rachum

Bu C ++ 98'de nasıl çalışır? CINT yorumlayıcıda hatalar alıyorum ve bunun C ++ 98 veya CINT'in kendisinden kaynaklandığını merak ediyordum ...!
xcorat

16

Bunun gibi bir nesneyi döndürmeniz yeterlidir:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Bu, Things üzerinde kopya oluşturucuyu çağırır, böylece bunu kendi uygulamanızı yapmak isteyebilirsiniz. Bunun gibi:

Thing(const Thing& aThing) {}

Bu biraz daha yavaş gerçekleşebilir, ancak hiç sorun olmayabilir.

Güncelleme

Derleyici muhtemelen kopya oluşturucuya yapılan çağrıyı optimize eder, böylece fazladan ek yük olmaz. (Yorumda işaret edilen dreamlax gibi).


9
Thing thing();Thingayrıca yerel bir işlev döndürür a , ayrıca standart, derleyicinin sunduğunuz durumda kopya yapıcısını atlamasına izin verir; Herhangi bir modern derleyici muhtemelen bunu yapacaktır.
dreamlax

1
Özellikle derin bir kopya gerekiyorsa, kopya oluşturucuyu uygulayarak iyi bir noktaya gelebilirsiniz.
mbadawi23

@Dreamlax'ın belirttiği gibi, derleyici büyük olasılıkla kopya yapıcısına gerçekten gerekli olmayan bir çağrıyı önleyen işlevler için dönen kodu "optimize edecek" olsa da, kopya oluşturucu hakkında açıkça belirtmek için +1.
jose.angel.jimenez

2018'de, VS 2017'de, hareket yapıcısını kullanmaya çalışıyor. Taşıma yapıcısı silinirse ve kopya yapıcısı derlenmezse derlenmez.
Andrew

11

Auto_ptr gibi akıllı işaretçiler (Thing gerçekten büyük ve ağır bir nesne ise) kullanmaya çalıştınız mı:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}

4
auto_ptrs kullanımdan kaldırılmıştır; kullanmak shared_ptrveya unique_ptryerine.
MBraedley

Sadece buraya ekleyeceğim ... C ++ ile profesyonel olarak olmasa da yıllardır c ++ kullanıyorum ... Artık akıllı işaretçiler kullanmamaya karar verdim, sadece mutlak bir karışıklık imo ve tüm nedenlere bu tür problemler, kodu da çok hızlandırmaya yardımcı olmuyor. RAII kullanarak sadece verileri kopyalayıp işaretçileri kendim yönetmeyi tercih ederim. Bu yüzden eğer mümkünse akıllı işaretçilerden kaçınmanızı tavsiye ederim.
Andrew

8

Bir kopya oluşturucunun çağrılıp çağrılmadığını belirlemenin hızlı bir yolu, sınıfınızın kopya yapıcısına günlük eklemektir:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Ara someFunction; alacağınız "Kopya yapıcısı çağrıldı" satırlarının sayısı 0, 1 ve 2 arasında değişecektir. Hiçbirini almazsanız, derleyiciniz dönüş değerini optimize etmiştir (yapmasına izin verilir). Eğer 0 alamadım olsun ve kopyalama yapıcı gülünç pahalı ise, o zaman da işlevlerden örnekleri dönmek için alternatif yollar aramak.


1

Öncelikle kodda bir hata var Thing *thing(new Thing());, sadece ve sadece return thing;.

  • Kullanın shared_ptr<Thing>. Bir işaretçi olarak tho deref. İçerdiği son referans Thingkapsam dışına çıktığında sizin için silinecektir .
  • İlk çözüm naif kütüphanelerde çok yaygındır. Biraz performansa ve sözdizimsel yüke sahiptir, mümkünse kaçının
  • İkinci çözümü yalnızca istisnaların atılmayacağını garanti edemiyorsanız veya performans kesinlikle kritik olduğunda kullanın (bu konuyla ilgili olmadan önce C veya montajla arayüz oluşturacaksınız).

0

Bir C ++ uzmanının daha iyi bir cevapla geleceğinden eminim, ama şahsen ikinci yaklaşımı seviyorum. Akıllı işaretçiler kullanmak, unutmak problemine yardımcı olur deleteve dediğiniz gibi, elden önce bir nesne oluşturmak zorunda kalmadan daha temiz görünür (ve yığın üzerinde ayırmak istiyorsanız yine de silmek zorunda kalırsınız).

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.