Czy praktyka zwracania zmiennej referencyjnej C ++ jest zła?


341

Myślę, że to trochę subiektywne; Nie jestem pewien, czy opinia będzie jednomyślna (widziałem wiele fragmentów kodu, w których zwracane są odwołania).

Zgodnie z komentarzem do tego pytania, które właśnie zadałem, dotyczące inicjowania referencji , zwracanie referencji może być złe, ponieważ [jak rozumiem] ułatwia to pominięcie usunięcia, co może prowadzić do wycieków pamięci.

Martwi mnie to, bo podążałem za przykładami (chyba, że ​​coś sobie wyobrażam) i robiłem to w kilku uczciwych miejscach ... Czy źle zrozumiałem? Czy to jest złe? Jeśli tak, to jak złe?

Wydaje mi się, że z powodu mojego mieszanego zestawu wskaźników i odniesień, w połączeniu z faktem, że jestem nowy w C ++, oraz całkowitym zamieszaniem co do tego, kiedy użyć, gdy moje aplikacje muszą być piekłem pamięci ...

Rozumiem również, że używanie inteligentnych / współdzielonych wskaźników jest ogólnie akceptowane jako najlepszy sposób uniknięcia wycieków pamięci.


Nie jest złe, jeśli piszesz funkcje / metody podobne do getterów.
John Z. Li

Odpowiedzi:


411

Zasadniczo zwracanie referencji jest całkowicie normalne i odbywa się przez cały czas.

Jeśli masz na myśli:

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

To wszelkiego rodzaju zło. Przydział stosu izniknie, a ty nie odnosisz się do niczego. To też jest złe:

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

Ponieważ teraz klient musi w końcu zrobić coś dziwnego:

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

Zauważ, że odniesienia do wartości są nadal tylko odniesieniami, więc wszystkie złe aplikacje pozostają takie same.

Jeśli chcesz przydzielić coś, co wykracza poza zakres funkcji, użyj inteligentnego wskaźnika (lub ogólnie kontenera):

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

A teraz klient przechowuje inteligentny wskaźnik:

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

Referencje są również w porządku, aby uzyskać dostęp do rzeczy, o których wiesz, że całe życie jest otwarte na wyższym poziomie, np .:

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

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

Wiemy tutaj, że można odwoływać się do odwołania, i_ponieważ to, co nas wzywa, zarządza czasem życia instancji klasy, więc i_będzie żyła przynajmniej tak długo.

I oczywiście nie ma nic złego w samym tylko:

int getInt() {
   return 0;
}

Jeśli czas życia należy do dzwoniącego, a ty po prostu obliczasz wartość.

Podsumowanie: można zwrócić referencję, jeśli czas życia obiektu nie skończy się po wywołaniu.


21
To wszystkie złe przykłady. Najlepszym przykładem prawidłowego użycia jest zwracanie referencji do obiektu, który został przekazany. Ala operator <<
Arelius

171
Ze względu na potomstwo i dla wszystkich nowszych programistów, którzy na to czekają , wskaźniki nie są złe . Wskaźniki do pamięci dynamicznej też nie są złe. Oboje mają swoje legalne miejsca w C ++. Inteligentne wskaźniki powinny zdecydowanie być domyślnym wyborem, jeśli chodzi o dynamiczne zarządzanie pamięcią, ale domyślnym inteligentnym wskaźnikiem powinien być unikalny_ptr, a nie współużytkowany_ptr.
Jamin Gray

12
Edytuj osoby zatwierdzające: nie zatwierdzaj zmian, jeśli nie możesz ręczyć za ich poprawność. Wycofałem niepoprawną edycję.
GManNickG

7
Ze względu na potomstwo i wszystkich nowszych programistów, którzy na to czekają, nie piszreturn new int .
Wyścigi lekkości na orbicie

3
Dla potomności i dla wszystkich nowszych programistów, którzy na to czekają, po prostu zwróć T z funkcji. RVO zajmie się wszystkim.
But

64

Nie, nie, tysiąc razy nie.

To, co jest złem, odnosi się do dynamicznie przydzielanego obiektu i traci oryginalny wskaźnik. Kiedy jesteś newprzedmiotem, zobowiązujesz się mieć gwarancję delete.

Ale spójrz np. Na operator<<: to musi zwrócić referencję, lub

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

nie zadziała.


23
Zgłosiłem się negatywnie, ponieważ ani to nie odpowiada na pytanie (w którym OP wyjaśnił, że wie o potrzebie usunięcia), ani nie odnosi się do uzasadnionego strachu, że powrót odniesienia do obiektu Freestore może prowadzić do zamieszania. Westchnienie.

4
Praktyka zwracania obiektu referencyjnego nie jest zła. Ergo nie. Strach, który wyraża, jest właściwym strachem, jak wskazałem w drugim grafie.
Charlie Martin,

Właściwie to nie zrobiłeś. Ale to nie jest warte mojego czasu.

2
Iraimbilanja @ O „Nie” - nie dbam o to. ale ten post wskazał na ważną informację, której brakowało w GMan.
Kobor42

48

Powinieneś zwrócić odniesienie do istniejącego obiektu, który nie odchodzi natychmiast i gdzie nie zamierzasz przenosić prawa własności.

Nigdy nie zwracaj odwołania do zmiennej lokalnej lub innej takiej, ponieważ nie będzie tam odniesienia.

Możesz zwrócić odwołanie do czegoś niezależnego od funkcji, którego nie oczekujesz, że funkcja wywołująca przejmie odpowiedzialność za usunięcie. Dotyczy to typowej operator[]funkcji.

Jeśli coś tworzysz, powinieneś zwrócić wartość lub wskaźnik (zwykły lub inteligentny). Możesz swobodnie zwrócić wartość, ponieważ przechodzi ona do zmiennej lub wyrażenia w funkcji wywołującej. Nigdy nie zwracaj wskaźnika do zmiennej lokalnej, ponieważ ona zniknie.


1
Doskonała odpowiedź, ale dla „Możesz zwrócić tymczasowe jako stałe odniesienie”. Poniższy kod zostanie skompilowany, ale prawdopodobnie ulegnie awarii, ponieważ tymczasowe jest zniszczone na końcu instrukcji return: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} „
j_random_hacker

@j_random_hacker: C ++ ma dziwne reguły dotyczące odniesień do tymczasowych, w których tymczasowe życie może zostać przedłużone. Przykro mi, ale nie rozumiem tego wystarczająco dobrze, aby wiedzieć, czy dotyczy twojej sprawy.
Mark Ransom

3
@ Mark: Tak, ma dziwne zasady. Żywotność tymczasowego można wydłużyć tylko poprzez zainicjowanie z nim stałej referencji (która nie jest członkiem klasy); następnie żyje, dopóki referencja nie wykracza poza zakres. Niestety, zwracanie stałej referencji nie jest objęte gwarancją. Zwracanie temp według wartości jest jednak bezpieczne.
j_random_hacker

Patrz Standard C ++, 12.2, akapit 5. Zobacz także zbłąkanego Guru Tygodnia Herb Suttera na stronie herbutter.wordpress.com/2008/01/01/… .
David Thornley,

4
@David: Gdy typem zwracanym przez funkcję jest „T const &”, tak naprawdę dzieje się tak, że instrukcja return domyślnie konwertuje temp, który jest typu T, na typ „T const &” zgodnie z 6.6.3.2 (prawna konwersja, ale jedna co nie wydłuża okresu użytkowania), a następnie kod wywołujący inicjuje ref typu „T const &” z wynikiem funkcji, również typu „T const &” - ponownie, legalny, ale nie przedłużający życia proces. Rezultat końcowy: bez przedłużenia życia i wiele zamieszania. :(
j_random_hacker

26

Uważam, że odpowiedzi nie są satysfakcjonujące, więc dodam dwa centy.

Przeanalizujmy następujące przypadki:

Błędne użycie

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

To oczywiście błąd

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

Zastosowanie ze zmiennymi statycznymi

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

Ma to rację, ponieważ zmienne statyczne istnieją przez cały okres istnienia programu.

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

Jest to również dość powszechne przy wdrażaniu wzorca Singleton

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

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

}

Stosowanie:

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

Operatorzy

Standardowe kontenery biblioteczne w dużym stopniu zależą na przykład od operatorów, które zwracają referencje

T & operator*();

może być użyty w następujący sposób

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

Szybki dostęp do danych wewnętrznych

Są chwile, kiedy & może być wykorzystany do szybkiego dostępu do danych wewnętrznych

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

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

z użyciem:

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

JEDNAK może to prowadzić do takich pułapek:

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!

Zwracanie odniesienia do zmiennej statycznej może prowadzić do niepożądanego zachowania, np. Rozważ operatora mnożenia, który zwraca odniesienie do elementu statycznego, wówczas zawsze będzie trueIf((a*b) == (c*d))
następować

Container::data()implementacja powinna brzmiećreturn m_data;
Xeaz

To było bardzo pomocne, dzięki! @Xeaz nie spowodowałoby to jednak problemów z dołączeniem połączenia?
Andrew

@SebTu Dlaczego w ogóle chcesz to zrobić?
Thorhunter

@Andrew Nie, to była składnia shenanigan. Jeśli na przykład zwróciłeś typ wskaźnika, użyjesz adresu referencyjnego, aby utworzyć i zwrócić wskaźnik.
Thorhunter

14

To nie jest złe. Podobnie jak wiele rzeczy w C ++, jest dobrze, jeśli jest używany poprawnie, ale istnieje wiele pułapek, o których powinieneś pamiętać podczas korzystania z niego (np. Zwracanie odwołania do zmiennej lokalnej).

Są dobre rzeczy, które można z nim osiągnąć (np. Map [name] = "hello world")


1
Jestem tylko ciekawa, co jest dobrego map[name] = "hello world"?
wrongusername

5
@wrongusername Składnia jest intuicyjna. Czy kiedykolwiek próbowałeś zwiększyć liczbę wartości przechowywanych HashMap<String,Integer>w Javie? : P
Mehrdad Afshari,

Haha, jeszcze nie, ale patrząc na przykłady HashMap, wygląda dość
sromotnie

Problem, który miałem z tym: Funkcja zwraca odwołanie do obiektu w kontenerze, ale kod funkcji wywołującej przypisał go do zmiennej lokalnej. Następnie zmodyfikowano niektóre właściwości obiektu. Problem: Oryginalny obiekt w pojemniku pozostał nietknięty. Programista tak łatwo przeoczy wartość zwracaną przez &, a wtedy otrzymasz naprawdę nieoczekiwane zachowania ...
flohack

10

„zwrócenie referencji jest złe, ponieważ po prostu [jak rozumiem] ułatwia to pominięcie jej usunięcia”

Nie prawda. Zwrócenie referencji nie oznacza semantyki własności. To dlatego, że robisz to:

Value& v = thing->getTheValue();

... nie oznacza, że ​​posiadasz teraz pamięć, o której mowa w v;

Jest to jednak okropny kod:

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

Jeśli robisz coś takiego, ponieważ „nie potrzebujesz wskaźnika w tej instancji”, to: 1) po prostu odłóż wskaźnik, jeśli potrzebujesz odniesienia, i 2) w końcu będziesz potrzebować wskaźnika, ponieważ musisz dopasować nowy z usunięciem, a do wywołania usunięcia potrzebny jest wskaźnik.


7

Istnieją dwa przypadki:

  • const reference - dobry pomysł, czasem, szczególnie dla ciężkich obiektów lub klas proxy, optymalizacja kompilatora

  • non-const reference - zły pomysł, czasami psuje enkapsulacje

Oba mają ten sam problem - mogą potencjalnie wskazywać na zniszczony obiekt ...

Polecam używanie inteligentnych wskaźników w wielu sytuacjach, w których musisz zwrócić referencję / wskaźnik.

Zwróć również uwagę na następujące kwestie:

Istnieje formalna reguła - Standard C ++ (sekcja 13.3.3.1.4, jeśli jesteś zainteresowany) stwierdza, że ​​tymczasowe może być powiązane tylko z odwołaniem do stałej - jeśli spróbujesz użyć odwołania do stałej, kompilator musi oznaczyć to jako błąd.


1
non-const ref niekoniecznie przerywa enkapsulację. rozważyć wektor :: operator []

to bardzo szczególny przypadek ... dlatego czasami mówiłem, chociaż powinienem naprawdę twierdzić, że WIĘKSZOŚĆ CZASU :)

Mówisz więc, że normalne wdrożenie operatora indeksu dolnego jest koniecznym złem? Nie zgadzam się z tym ani się z tym nie zgadzam; bo ja nie jestem mądrzejszy.
Nick Bolton,

Nie mówię tego, ale może być zły, jeśli zostanie niewłaściwie wykorzystany :))) wektor :: należy używać, gdy tylko jest to możliwe ....

co? vector :: at zwraca także niekonsekwentny ref.

4

Nie tylko nie jest zły, ale czasami jest niezbędny. Na przykład niemożliwe byłoby zaimplementowanie operatora [] std :: vector bez użycia zwracanej wartości referencyjnej.


Ach tak, oczywiście; Myślę, że dlatego zacząłem go używać; kiedy po raz pierwszy wdrożyłem operator indeksu dolnego [] zdałem sobie sprawę z użycia referencji. Jestem przekonany, że to jest de facto.
Nick Bolton,

Co dziwne, możesz zaimplementować operator[]kontener bez użycia odwołania ... i std::vector<bool>robi to. (I w ten sposób powstaje prawdziwy bałagan)
Ben Voigt

@BenVoigt mmm, po co bałagan? Zwracanie proxy jest również prawidłowym scenariuszem dla kontenerów ze złożoną pamięcią, które nie są mapowane bezpośrednio na typy zewnętrzne (jak ::std::vector<bool>wspomniano).
Sergey.quixoticaxis.Ivanov

1
@ Sergey.quixoticaxis.Ivanov: Bałagan polega na tym, że użycie std::vector<T>kodu szablonu jest zepsute, jeśli Tmoże być bool, ponieważ std::vector<bool>ma inne zachowanie niż inne instancje. Jest to przydatne, ale powinno się nadać mu własną nazwę, a nie specjalizację std::vector.
Ben Voigt,

@BenVoight Zgadzam się co do dziwnej decyzji o uczynieniu jednej specjalizacji „naprawdę wyjątkową”, ale czułem, że twój oryginalny komentarz sugeruje, że zwrot proxy jest ogólnie dziwny.
Sergey.quixoticaxis.Ivanov

2

Dodatek do zaakceptowanej odpowiedzi:

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

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

Twierdziłbym, że ten przykład jest nie w porządku i należy go w miarę możliwości unikać. Dlaczego? Bardzo łatwo jest uzyskać zwisające odniesienie .

Aby zilustrować tę kwestię przykładem:

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

wejście do strefy zagrożenia:

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

1

referencja powrotu jest zwykle używana w przypadku przeciążenia operatora w C ++ dla dużych obiektów, ponieważ zwracanie wartości wymaga operacji kopiowania. (w przeciążeniu peratora zwykle nie używamy wskaźnika jako wartości zwracanej)

Ale odwołanie zwrotne może powodować problem z alokacją pamięci. Ponieważ odwołanie do wyniku zostanie przekazane z funkcji jako odniesienie do wartości zwracanej, wartość zwracana nie może być zmienną automatyczną.

jeśli chcesz użyć zwracanej wartości, możesz użyć bufora obiektu statycznego. na przykład

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;
 }

w ten sposób możesz bezpiecznie korzystać ze zwracanego odwołania.

Ale zawsze możesz użyć wskaźnika zamiast odwołania do zwracania wartości w functiong.


0

myślę, że używanie referencji jako wartości zwracanej przez funkcję jest znacznie prostsze niż używanie wskaźnika jako wartości zwracanej przez funkcję. Po drugie, zawsze byłoby bezpiecznie używać zmiennej statycznej, do której odnoszą się wartości zwracane.


0

Najlepiej jest utworzyć obiekt i przekazać go jako parametr referencyjny / wskaźnikowy do funkcji, która przydziela tę zmienną.

Przydzielanie obiektu w funkcji i zwracanie go jako odwołania lub wskaźnika (wskaźnik jest jednak bezpieczniejszy) jest złym pomysłem ze względu na zwolnienie pamięci na końcu bloku funkcyjnego.


-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 
     }
  };

Funkcja getPtr może uzyskać dostęp do pamięci dynamicznej po usunięciu, a nawet zerowego obiektu. Co może powodować wyjątki złego dostępu. Zamiast tego należy wprowadzić getter i setter, a przed zwrotem sprawdzić rozmiar.


-2

Funkcja jako lvalue (czyli zwracanie odwołań nie stałych) powinna zostać usunięta z C ++. To jest wyjątkowo nieintuicyjne. Scott Meyers chciał min () z tym zachowaniem.

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

co nie jest tak naprawdę poprawą

setmin (a, b, 0);

To drugie ma nawet większy sens.

Zdaję sobie sprawę, że funkcja jako wartość jest ważna dla strumieni w stylu C ++, ale warto zauważyć, że strumienie w stylu C ++ są okropne. Nie jestem jedynym, który tak myśli ... jak pamiętam Alexandrescu miał duży artykuł na temat tego, jak robić lepiej, i uważam, że boost próbował również stworzyć lepszą metodę bezpiecznych I / O.


2
Pewnie, że jest to niebezpieczne i powinno istnieć lepsze sprawdzanie błędów kompilatora, ale bez niego nie można byłoby wykonać kilku użytecznych konstrukcji, np. Operator [] () w std :: map.
j_random_hacker

2
Zwracanie referencji innych niż stałe jest niezwykle przydatne. vector::operator[]na przykład. Wolisz pisać v.setAt(i, x)czy v[i] = x? Ten ostatni jest lepszy od FAR.
Miles Rout

1
@MilesRout Pójdę na v.setAt(i, x)zawsze. Jest o wiele lepszy.
scravy

-2

Natknąłem się na prawdziwy problem, w którym to rzeczywiście było złe. Zasadniczo programista zwrócił odwołanie do obiektu w wektorze. To było złe!!!

Pełne szczegóły, o których pisałem w Janurary: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Jeśli musisz zmodyfikować oryginalną wartość w kodzie wywołującym, musisz zwrócić numer referencyjny. I to w rzeczywistości nie jest bardziej i nie mniej niebezpieczne niż powrót iteratora do wektora - oba są unieważniane, jeśli elementy zostaną dodane lub usunięte z wektora.
j_random_hacker

Ten szczególny problem został spowodowany utrzymywaniem odwołania do elementu wektora, a następnie modyfikowaniem tego wektora w sposób, który unieważnia odwołanie: Strona 153, sekcja 6.2 „Standardowej biblioteki C ++: samouczek i odniesienie” - Josuttis brzmi: „Wstawianie lub usunięcie elementów unieważnia odniesienia, wskaźniki i iteratory, które odnoszą się do następujących elementów. Jeżeli wstawienie powoduje realokację, unieważnia wszystkie odniesienia, iteratory i wskaźniki ”
Trent

-15

O okropnym kodzie:

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

Rzeczywiście wskaźnik pamięci utracony po powrocie. Ale jeśli używasz share_ptr w ten sposób:

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

Pamięć nie zostanie utracona po powrocie i zostanie zwolniona po przypisaniu.


12
Zostaje utracony, ponieważ wspólny wskaźnik wychodzi poza zasięg i uwalnia liczbę całkowitą.

wskaźnik nie zostanie utracony, adresem odniesienia jest wskaźnik.
dgsomerton
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.