Bir C ++ referans değişkeni döndürme uygulaması kötü mü?


341

Bu biraz öznel olduğunu düşünüyorum; Ben görüş oybirliği ile emin değilim (ben başvuruları döndürülür kod parçacıkları bir sürü gördüm).

Bu soruyla ilgili bir açıklamaya göre , referansların başlatılmasıyla ilgili olarak , bir referansın geri döndürülmesi kötü olabilir, çünkü [anladığım kadarıyla] onu silmeyi kolaylaştırır, bu da bellek sızıntılarına neden olabilir.

Örnekleri takip ettiğim için (beni hayal etmiyorsam) ve bunu adil bir yerde yapmadım, bu beni endişelendiriyor ... Yanlış anladım mı? Kötü mü? Eğer öyleyse, ne kadar kötülük?

C ++ için yeni olduğum gerçeği ve ne zaman kullanacağım konusunda toplam karışıklık ile birleştiğim karışık işaretçiler ve referans çantam, uygulamalarımın bellek sızıntısı olması gerektiğini hissediyorum ...

Ayrıca, akıllı / paylaşılan işaretçiler kullanmanın genellikle bellek sızıntılarını önlemenin en iyi yolu olduğunu kabul ediyorum.


Alıcı benzeri işlevler / yöntemler yazıyorsanız kötü değil.
John Z. Li

Yanıtlar:


411

Genel olarak, bir referansın döndürülmesi tamamen normaldir ve her zaman olur.

Şunu mu demek istediniz:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

Bu her türlü kötülüktür. Yığın tahsisi igider ve siz hiçbir şeyden bahsedersiniz. Bu da kötüdür:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Çünkü şimdi müşteri garip bir şekilde yapmak zorunda:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Değer referanslarının hala sadece referans olduğunu unutmayın, bu nedenle tüm kötü uygulamalar aynı kalır.

İşlevin kapsamı dışında kalan bir şey ayırmak istiyorsanız, akıllı bir işaretçi (veya genel olarak bir kap) kullanın:

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

Ve şimdi müşteri akıllı bir işaretçi saklıyor:

std::unique_ptr<int> x = getInt();

Referanslar, yaşam süresinin daha yüksek bir seviyede açık tutulduğunu bildiğiniz şeylere erişmek için de uygundur, örneğin:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Burada referansı geri göndermenin uygun olduğunu biliyoruz, i_çünkü bizi arayan her şey sınıf örneğinin ömrünü yönetiyor, bu yüzden i_en azından bu kadar uzun yaşayacak.

Ve elbette, sadece yanlış bir şey yok:

int getInt() {
   return 0;
}

Ömür boyu arayana bırakılmalı ve sadece değeri hesaplıyorsanız.

Özet: Nesnenin ömrü çağrıdan sonra bitmezse bir referans döndürmeniz uygundur.


21
Bunların hepsi kötü örnekler. Doğru kullanım için en iyi örnek, aktarılan bir nesneye bir referans döndürürken
verilebilir

171
Gelecek kuşaklar için ve bununla ilgili yeni programcılar için işaretçiler kötü değildir . Dinamik belleğe işaretçiler de kötü değildir. Her ikisinin de C ++ 'da meşru yerleri var. Akıllı işaretçiler dinamik bellek yönetimi söz konusu olduğunda kesinlikle varsayılan tercihiniz olmalıdır, ancak varsayılan akıllı işaretçiniz shared_ptr değil, unique_ptr olmalıdır.
Jamin Gray

12
Onaylayanları düzenle: doğruluğunu onaylayamıyorsanız düzenlemeleri onaylamayın. Yanlış düzenlemeyi geri aldım.
GManNickG

7
Gelecek kuşaklar için ve bununlareturn new int ilgili yeni programcılar için yazmayın .
Yörüngedeki Hafiflik Yarışları

3
Posterity uğruna ve bununla ilgili yeni programcılar için T'yi işlevden geri döndürmeniz yeterlidir. RVO her şeyi halleder.
Ayakkabı

64

Hayır. Hayır, hayır, bin kere hayır.

Kötülük, dinamik olarak tahsis edilmiş bir nesneye gönderme yapmak ve orijinal işaretçiyi kaybetmektir. Bir newnesne olduğunuzda garantiye sahip olma yükümlülüğünüzü üstlenirsiniz delete.

Ama, örneğin bir göz, operator<<: o olmalıdır başvuru döndürmek veya

cout << "foo" << "bar" << "bletch" << endl ;

çalışmaz.


23
Aşağıdakileri reddettim, çünkü bu ne OP'nin (silme ihtiyacını bildiğini açıkça belirttiği) ne de bir freestore nesnesine bir referans döndürmenin karışıklığa yol açabileceğine dair meşru korkuyu ele alıyor. İç çekmek.

4
Bir referans nesneyi döndürme pratiği kötü değildir . Ergo no. İkinci grafiğe işaret ettiğim gibi, ifade ettiği korku doğru bir korkudur.
Charlie Martin

Aslında yapmadın. Ama bu benim zamanım değil.

2
Iraimbilanja @ "Hayır" hakkında -Bir umurumda değil. ancak bu yazı GMan'dan eksik olan önemli bir bilgiye dikkat çekti.
Kobor42

48

Hemen gitmeyen ve herhangi bir sahiplik aktarımı yapmak istemediğiniz mevcut bir nesneye başvuru göndermelisiniz.

Asla yerel bir değişkene veya bazılarına bir referans döndürmeyin, çünkü referans alınmayacaktır.

Arama işlevinin silme sorumluluğunu almasını beklemediğiniz işlevden bağımsız bir şeye başvuru döndürebilirsiniz. Tipik operator[]fonksiyon için durum böyledir .

Bir şey oluşturuyorsanız, bir değer veya işaretçi (normal veya akıllı) döndürmelisiniz. Çağıran işlevde bir değişkene veya ifadeye girdiği için bir değeri serbestçe döndürebilirsiniz. İşaretçiyi asla yerel bir değişkene döndürmeyin, çünkü bu işlev kaybolacaktır.


1
Mükemmel yanıt ama "Geçici bir başvuru olarak geçici bir süre dönebilirsiniz." Aşağıdaki kod derlenecek, ancak geçici olarak return ifadesinin sonunda imha edildiğinden çökecektir: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker: C ++ 'da geçici ömürlerin uzatılabileceği geçici arabirimlere başvurular için bazı garip kurallar vardır. Üzgünüm, davanızı kapsayıp kapsamadığını bilmek için yeterince iyi anlamadım.
Mark Ransom

3
@ Mark: Evet, bazı garip kuralları var. Geçici bir kişinin ömrü ancak onunla birlikte bir const referansı (sınıf üyesi olmayan) başlatılarak uzatılabilir; daha sonra ref kapsam dışına çıkana kadar yaşar. Ne yazık ki, bir const ref döndürme kapsamında değildir . Ancak bir değerin değere döndürülmesi güvenlidir.
j_random_hacker

C ++ Standard, 12.2 Bkz paragraf 5. Ayrıca en Hafta Herb Sutter'ın başıboş Guru bkz herbsutter.wordpress.com/2008/01/01/... .
David Thornley

4
@ David: işlevin dönüş türü "T const &" olduğunda, aslında ne olur Dönüş ifadesi olduğunu örtük dönüştüren 6.6.3.2 (yasal dönüşüm ancak biri uyarınca "& T const" yazmak için, T türünde olduğu temp, çağrı kodu "T const &" türünün ref'sini işlevin sonucuyla, yine "T const &" türünden - yine yasal ancak ömrünü uzatan bir işlemle başlatır. Sonuç: ömür boyu uzama ve karışıklık yok. :(
j_random_hacker

26

Cevapları tatmin edici bulmuyorum, bu yüzden iki sentimi ekleyeceğim.

Aşağıdaki durumları analiz edelim:

Hatalı kullanım

int& getInt()
{
    int x = 4;
    return x;
}

Bu açıkça bir hatadır

int& x = getInt(); // will refer to garbage

Statik değişkenlerle kullanım

int& getInt()
{
   static int x = 4;
   return x;
}

Bu doğrudur, çünkü statik değişkenler bir programın ömrü boyunca vardır.

int& x = getInt(); // valid reference, x = 4

Bu, Singleton kalıbını uygularken de oldukça yaygındır

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Kullanımı:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

Operatörler

Standart kütüphane kapları, büyük ölçüde referans veren operatörlerin kullanımına bağlıdır.

T & operator*();

aşağıdakilerde kullanılabilir

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Dahili verilere hızlı erişim

Dahili verilere hızlı erişim için kullanılabilecek ve kullanılabilecek zamanlar vardır

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

kullanım ile:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

ANCAK, bu böyle bir tuzağa neden olabilir:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!

Başvurunun statik bir değişkene döndürülmesi istenmeyen davranışlara yol açabilir, örneğin statik bir üyeye başvuru döndüren bir çarpma işlecini düşünün, sonra aşağıdakiler her zaman şu şekilde sonuçlanır true:If((a*b) == (c*d))
SebNag

Container::data()adlı kullanıcının uygulaması şunu okumalıdırreturn m_data;
Xeaz

Bu çok yardımcı oldu, teşekkürler! @Xeaz, ekleme çağrısı ile ilgili sorunlara neden olmaz mı?
Andrew

@SebTu Bunu neden yapmak istesin ki?
thorhunter

@Andrew Hayır, bir sözdizimi shenanigan'dı. Örneğin, bir işaretçi türü döndürdüyseniz, bir işaretçi oluşturmak ve döndürmek için referans adresini kullanırsınız.
thorhunter

14

Kötü değil. C ++ 'daki birçok şey gibi, doğru kullanılırsa iyidir, ancak kullanırken dikkat etmeniz gereken birçok tuzak vardır (yerel bir değişkene referans döndürmek gibi).

Bununla başarılabilecek iyi şeyler var (map [name] = "merhaba dünya" gibi)


1
Sadece merak ediyorum, neyin iyi map[name] = "hello world"?
yanlış kullanıcı adı

5
@wrongusername Sözdizimi sezgiseldir. Hiç HashMap<String,Integer>Java'da depolanan bir değerin sayısını artırmaya çalıştınız mı? : P
Mehrdad Afshari

Haha, henüz değil, ama HashMap örneklerine baktığımızda, oldukça gnarly görünüyor: D
yanlış kullanıcı adı

Sorun bu ile vardı: İşlev bir kapsayıcıdaki bir nesneye başvuru döndürür, ancak çağıran işlev kodu yerel bir değişkene atandı. Sonra nesnenin bazı özelliklerini değiştirdi. Sorun: Kaptaki orijinal nesneye dokunulmadı. Programcı dönüş değerine kolayca & bakar, ve sonra gerçekten beklenmedik davranışlar elde ...
flohack

10

"bir referansı döndürmek kötüdür, çünkü basitçe [anladığım kadarıyla], onu silmeyi özlemeyi kolaylaştırır"

Doğru değil. Bir referansın döndürülmesi sahiplik semantiği anlamına gelmez. Sadece bunu yaptığınız için:

Value& v = thing->getTheValue();

... artık v ile belirtilen belleğe sahip olduğunuz anlamına gelmez;

Ancak, bu korkunç bir kod:

int& getTheValue()
{
   return *new int;
}

Böyle bir şey yapıyorsanız, çünkü "o örnekte bir işaretçiye ihtiyacınız yoktur" : o zaman: 1) bir referansa ihtiyacınız varsa işaretçiyi serbest bırakın ve 2) sonunda işaretçiye ihtiyacınız olacaktır, çünkü bir bir delete ile yeni ve delete aramak için bir işaretçiye ihtiyacınız var.


7

İki durum söz konusudur:

  • const referans - iyi fikir, bazen, özellikle ağır nesneler veya proxy sınıfları, derleyici optimizasyonu

  • const olmayan referans - kötü fikir bazen kapsüllemeyi keser

Her ikisi de aynı sorunu paylaşıyor - potansiyel olarak yok edilen nesneye işaret edebilir ...

Bir referans / işaretçi döndürmeniz gereken birçok durum için akıllı işaretçiler kullanmanızı öneririm.

Ayrıca, aşağıdakilere dikkat edin:

Resmi bir kural vardır - C ++ Standardı (eğer ilgilenirseniz bölüm 13.3.3.1.4), bir geçici öğenin yalnızca const referansına bağlanabileceğini belirtir - const olmayan bir referans kullanmaya çalışırsanız derleyici bunu şöyle işaretlemelidir bir hata.


1
sabit olmayan ref zorunlu olarak kapsüllenmeyi bozmaz. vektör düşünün :: operatör []

bu çok özel bir durum ... bu yüzden bazen dedim, ama gerçekten ZAMANIN EN ÇOK talep etmeliyim :)

Yani, normal abone operatörü uygulamasının gerekli bir kötülük olduğunu mu söylüyorsunuz? Buna ne katılmıyorum ne de katılıyorum; çünkü ben daha akıllı değilim.
Nick Bolton

Ben söylemiyorum, ama kötüyse kötülük olabilir :))) vector :: at mümkün olduğunda kullanılmalıdır ....

ha? vector :: at ayrıca sabit olmayan bir ref döndürür.

4

Sadece kötülük değil, bazen de şarttır. Örneğin, std :: vector'un [] operatörünü bir referans dönüş değeri kullanmadan uygulamak imkansızdır.


Ah, evet elbette; Bence bu yüzden kullanmaya başladım; abonelik operatörünü ilk uyguladığımda olduğu gibi [] referansların kullanımını anladım. Bunun fiili olduğuna inanmaya başladım.
Nick Bolton

İşin garibi, operator[]bir başvuru kullanmadan bir kap için uygulayabilirsiniz ... ve std::vector<bool>yapar. (Ve bu süreçte gerçek bir karmaşa yaratıyor)
Ben Voigt

@BenVoigt mmm, neden bir karışıklık? Proxy döndürmek, doğrudan dış türlerle (belirttiğiniz gibi) eşleşmeyen karmaşık depolama alanına sahip kaplar için geçerli bir senaryodur ::std::vector<bool>.
Sergey.quixoticaxis.Ivanov

1
Sergey.quixoticaxis.Ivanov @: karışıklık kullanarak olmasıdır std::vector<T>eğer şablon kodunda, bozuldu Tolabilir bool, çünkü std::vector<bool>diğer örneklemesi çok farklı davranış vardır. Yararlı, ama bir uzmanlık değil, kendi adı verilmeliydi std::vector.
Ben Voigt

@BenVoight Ben bir uzmanlık "gerçekten özel" yapma garip karar konusunda anlaşıyorum ama orijinal yorumda bir proxy döndürmenin genel olarak garip olduğunu ima hissettim.
Sergey.quixoticaxis.Ivanov

2

Kabul edilen cevaba ek:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Bu örneğin iyi olmadığını ve mümkünse kaçınılması gerektiğini savunuyorum. Neden? Sarkan bir referansla son bulmak çok kolaydır .

Noktayı bir örnekle göstermek için:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

tehlike bölgesine girme:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!

1

dönüş referansı genellikle büyük Nesne için C ++ 'da aşırı yüklenen operatörde kullanılır, çünkü bir değer döndürmek kopyalama işlemine ihtiyaç duyar.

Ancak geri gönderme başvurusu bellek ayırma sorununa neden olabilir. Sonuca yapılan bir başvuru, dönüş değerine bir başvuru olarak işlevden geçirileceğinden, dönüş değeri otomatik bir değişken olamaz.

dönen refernce kullanmak istiyorsanız, statik nesne arabelleği kullanabilirsiniz. Örneğin

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

bu şekilde, geri dönen referansı güvenle kullanabilirsiniz.

Ancak functiong içinde değer döndürmek için her zaman başvuru yerine pointer kullanabilirsiniz.


0

Ben işlevin dönüş değeri olarak başvuru kullanarak işaretçinin işlevin dönüş değeri olarak kullanmak daha düz olduğunu düşünüyorum. İkincisi, dönüş değerinin bahsettiği statik değişkeni kullanmak her zaman güvenlidir.


0

En iyi şey, nesne oluşturmak ve onu bu değişkeni ayıran bir işleve referans / işaretçi parametresi olarak iletmektir.

İşlevdeki nesneyi ayırmak ve bir başvuru veya işaretçi olarak döndürmek (ancak işaretçi daha güvenlidir), işlev bloğunun sonunda bellek boşalması nedeniyle kötü bir fikirdir.


-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

getPtr işlevi, silindikten sonra dinamik belleğe hatta boş bir nesneye erişebilir. Bu, Kötü Erişim İstisnalarına neden olabilir. Bunun yerine alıcı ve ayarlayıcı uygulanmalı ve iade edilmeden önce boyut doğrulanmalıdır.


-2

Lvalue işlevi (diğer bir deyişle, sabit olmayan başvuruların döndürülmesi) C ++ 'dan kaldırılmalıdır. Son derece kasıtsız. Scott Meyers bu davranışla bir dakika () istedi.

min(a,b) = 0;  // What???

ki bu gerçekten bir gelişme değil

setmin (a, b, 0);

İkincisi daha mantıklı.

Ben lvalue olarak bu fonksiyonun C ++ tarzı akışlar için önemli olduğunu, ancak C ++ tarzı akışlarının korkunç olduğunu belirtmeye değer. Bunu düşünen tek kişi ben değilim ... Alexandrescu'nun nasıl daha iyi yapılacağına dair büyük bir makalesi olduğunu hatırladığım için, desteğin de daha iyi bir güvenli G / Ç yöntemi yaratmaya çalıştığına inanıyorum.


2
Elbette tehlikeli ve daha iyi bir derleyici hata kontrolü olmalı, ancak onsuz bazı yararlı yapılar yapılamadı, örneğin std :: map'de operatör [] ().
j_random_hacker

2
Sabit olmayan referansları döndürmek gerçekten inanılmaz derecede faydalıdır. vector::operator[]Örneğin. Bunun yerine yazma misiniz v.setAt(i, x)ya v[i] = x? İkincisi FAR üstündür.
Miles Rout

1
@MilesRout v.setAt(i, x)İstediğim zaman giderdim . Çok üstündür.
scravy

-2

Gerçekten kötü olan gerçek bir problemle karşılaştım. Temelde bir geliştirici, bir vektördeki bir nesneye bir başvuru döndürdü. O kötüydü!!!

Janurary'de yazdığım tüm ayrıntılar: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Arama kodundaki orijinal değeri değiştirmeniz gerekirse, bir ref döndürmeniz gerekir . Ve bu aslında bir yineleyiciyi bir vektöre döndürmekten daha fazla ve daha az tehlikeli değildir - her ikisi de vektöre öğeler eklenir veya vektörden çıkarılırsa her ikisi de geçersiz kılınır.
j_random_hacker

Bu sorun, bir vektör öğesine referans tutarak ve ardından bu vektörün referansı geçersiz kılacak şekilde değiştirilmesinden kaynaklandı: Sayfa 153, "C ++ Standart Kütüphane: Bir Öğretici ve Referans" - Josuttis, şöyle ekliyor: "Ekleme" veya öğelerin kaldırılması, aşağıdaki öğelere başvuran başvuruları, işaretçileri ve yineleyicileri geçersiz kılar. Bir ekleme yeniden tahsise neden olursa, tüm başvuruları, yineleyicileri ve işaretçileri geçersiz kılar "
Trent

-15

Korkunç kod hakkında:

int& getTheValue()
{
   return *new int;
}

Yani, gerçekten, bellek işaretçisi dönüşten sonra kayboldu. Ancak böyle paylaşılan_ptr kullanıyorsanız:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

Bellek geri döndükten sonra kaybolmaz ve atamadan sonra serbest bırakılır.


12
Paylaşılan işaretçi kapsam dışına çıktığından ve tamsayıyı serbest bıraktığı için kaybolur.

işaretçi kaybolmaz, referansın adresi işaretleyicidir.
dgsomerton
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.