Jak „zwrócić obiekt” w C ++?


167

Wiem, że tytuł brzmi znajomo, ponieważ jest wiele podobnych pytań, ale proszę o inny aspekt problemu (znam różnicę między posiadaniem rzeczy na stosie a układaniem ich na stosie).

W Javie zawsze mogę zwrócić odniesienia do obiektów „lokalnych”

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

W C ++, aby zrobić coś podobnego, mam 2 opcje

(1) Mogę używać referencji, gdy potrzebuję „zwrócić” obiekt

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

Następnie użyj tego w ten sposób

Thing thing;
calculateThing(thing);

(2) Lub mogę zwrócić wskaźnik do dynamicznie przydzielonego obiektu

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

Następnie użyj tego w ten sposób

Thing* thing = calculateThing();
delete thing;

Korzystając z pierwszego podejścia, nie będę musiał ręcznie zwalniać pamięci, ale dla mnie utrudnia to odczytanie kodu. Problem z drugim podejściem polega na tym, że będę musiał pamiętać delete thing;, co nie wygląda całkiem ładnie. Nie chcę zwracać skopiowanej wartości, ponieważ jest nieefektywna (tak mi się wydaje), więc oto pytania

  • Czy istnieje trzecie rozwiązanie (które nie wymaga kopiowania wartości)?
  • Czy jest jakiś problem, jeśli będę trzymać się pierwszego rozwiązania?
  • Kiedy i dlaczego powinienem skorzystać z drugiego rozwiązania?

32
+1 za ładne postawienie pytania.
Kangkan

1
Aby być bardzo pedantycznym, powiedzenie, że „funkcje coś zwracają” jest trochę nieprecyzyjne. Bardziej poprawnie, oszacowanie wywołania funkcji daje wartość . Wartość jest zawsze obiektem (chyba że jest to funkcja void). Różnica polega na tym, czy wartość jest wartością glvalue czy prvalue - co jest określane na podstawie tego, czy zadeklarowany typ zwracany jest referencją, czy nie.
Kerrek SB

Odpowiedzi:


107

Nie chcę zwracać skopiowanej wartości, ponieważ jest nieefektywna

Udowodnij to.

Wyszukaj RVO i NRVO oraz w semantyce ruchu C ++ 0x. W większości przypadków w C ++ 03 parametr out jest po prostu dobrym sposobem na uczynienie kodu brzydkim, aw C ++ 0x faktycznie krzywdziłbyś siebie używając parametru out.

Po prostu napisz czysty kod i zwróć według wartości. Jeśli problemem jest wydajność, sprofiluj ją (przestań zgadywać) i dowiedz się, co możesz zrobić, aby to naprawić. Prawdopodobnie nie zwróci rzeczy z funkcji.


To powiedziawszy, jeśli nie masz ochoty pisać w ten sposób, prawdopodobnie chciałbyś zrobić parametr out. Unika dynamicznej alokacji pamięci, co jest bezpieczniejsze i generalnie szybsze. Wymaga to sposobu na skonstruowanie obiektu przed wywołaniem funkcji, co nie zawsze ma sens dla wszystkich obiektów.

Jeśli chcesz korzystać z alokacji dynamicznej, możesz przynajmniej umieścić ją w inteligentnym wskaźniku. (I tak powinno być to robione przez cały czas) Wtedy nie martwisz się o usunięcie czegokolwiek, wszystko jest bezpieczne, itp. Jedynym problemem jest to, że prawdopodobnie i tak jest wolniejsze niż zwracanie wartości!


10
@phunehehe: Nie ma sensu spekulować, powinieneś sprofilować swój kod i dowiedzieć się. (Podpowiedź: nie.) Kompilatory są bardzo sprytne, nie będą tracić czasu na kopiowanie rzeczy, jeśli nie muszą. Nawet jeśli kopiowanie coś kosztuje, nadal powinieneś dążyć do dobrego kodu zamiast szybkiego; dobry kod można łatwo zoptymalizować, gdy problemem staje się szybkość. Nie ma sensu zepsuć kodu czegoś, o czym nie masz pojęcia, jest problemem; zwłaszcza jeśli faktycznie go spowolnisz lub nic z tego nie uzyskasz. A jeśli używasz C ++ 0x, semantyka ruchu sprawia, że ​​nie stanowi to problemu.
GManNickG

1
@GMan, re: RVO: w rzeczywistości jest to prawdą tylko wtedy, gdy twój rozmówca i wywoływany znajdują się w tej samej jednostce kompilacji, co w rzeczywistości nie jest przez większość czasu. Więc jesteś rozczarowany, jeśli twój kod nie jest w całości oparty na szablonach (w takim przypadku cały będzie w jednej jednostce kompilacji) lub masz pewną optymalizację czasu łącza (GCC ma ją tylko od 4.5).
Alex B

2
@Alex: Kompilatory coraz lepiej optymalizują jednostki tłumaczeniowe. (VC robi to teraz dla kilku wydań.)
sbi

9
@Alex B: To kompletna bzdura. Wiele bardzo powszechnych konwencji wywoływania powoduje, że wywołujący jest odpowiedzialny za przydzielanie miejsca dla dużych wartości zwracanych, a odbiorca jest odpowiedzialny za ich konstrukcję. RVO szczęśliwie działa w różnych jednostkach kompilacji, nawet bez optymalizacji czasu łącza.
CB Bailey

6
@Charles, po sprawdzeniu wydaje się, że jest poprawny! Wycofuję moje wyraźnie błędne oświadczenie.
Alex B

41

Po prostu utwórz obiekt i zwróć go

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

Myślę, że zrobisz sobie przysługę, jeśli zapomnisz o optymalizacji i po prostu napiszesz czytelny kod (będziesz musiał później uruchomić profiler - ale nie optymalizuj wstępnie).


2
Thing thing();deklaruje funkcję lokalną i zwraca plik Thing.
dreamlax

2
Rzecz () deklaruje funkcję zwracającą rzecz. W ciele funkcji nie ma obiektu Rzecz skonstruowanego.
CB Bailey,

@dreamlax @Charles @GMan Trochę późno, ale naprawiono.
Amir Rachum

Jak to działa w C ++ 98? Dostaję błędy na interpretatorze CINT i zastanawiałem się, że to z powodu C ++ 98 lub samego CINT ...!
xcorat

16

Po prostu zwróć obiekt taki jak ten:

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

Spowoduje to wywołanie konstruktora kopiującego w Things, więc możesz chcieć wykonać własną implementację tego. Lubię to:

Thing(const Thing& aThing) {}

Może to działać trochę wolniej, ale może w ogóle nie stanowić problemu.

Aktualizacja

Kompilator prawdopodobnie zoptymalizuje wywołanie konstruktora kopiującego, więc nie będzie dodatkowego narzutu. (Jak Dreamlax wskazał w komentarzu).


9
Thing thing();deklaruje lokalną funkcję zwracającą a Thing, ponadto standard zezwala kompilatorowi na pominięcie konstruktora kopiującego w przedstawionym przypadku; każdy nowoczesny kompilator prawdopodobnie to zrobi.
dreamlax

1
Wnosisz dobry punkt do implementacji konstruktora kopiującego, zwłaszcza jeśli potrzebna jest głęboka kopia.
mbadawi 23

+1 za jawne określenie konstruktora kopiującego, chociaż, jak mówi @dreamlax, kompilator najprawdopodobniej „zoptymalizuje” zwracany kod dla funkcji, unikając niepotrzebnego wywołania konstruktora kopiującego.
jose.angel.jimenez

W 2018, w VS 2017, próbuje użyć konstruktora przenoszenia. Jeśli konstruktor przenoszenia zostanie usunięty, a konstruktor kopiujący nie, nie zostanie skompilowany.
Andrew,

11

Czy próbowałeś użyć inteligentnych wskaźników (jeśli Rzecz jest naprawdę dużym i ciężkim obiektem), takich jak auto_ptr:


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 są przestarzałe; użyj shared_ptrlub unique_ptrzamiast.
MBraedley,

Dodam to tutaj ... Używam c ++ od lat, choć nie profesjonalnie z c ++ ... Postanowiłem już nie używać inteligentnych wskaźników, są po prostu absolutnym bałaganem i powodują wszystko różnego rodzaju problemy, które również nie pomagają zbytnio przyśpieszyć kodu. Wolałbym po prostu kopiować dane i samodzielnie zarządzać wskaźnikami, używając RAII. Dlatego radzę, jeśli możesz, unikaj mądrych wskazówek.
Andrew,

8

Jednym z szybkich sposobów ustalenia, czy wywoływany jest konstruktor kopiujący, jest dodanie logowania do konstruktora kopiującego Twojej klasy:

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

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

Zadzwoń someFunction; liczba wierszy „Kopiuj konstruktor został nazwany”, które otrzymasz, będzie się wahać między 0, 1 i 2. Jeśli żadne nie otrzymasz, oznacza to, że kompilator zoptymalizował zwracaną wartość out (co jest dozwolone). Jeśli masz nie dostać 0, a konstruktor kopia jest absurdalnie drogie, a następnie szukać alternatywnych sposobów, aby powrócić instancji od swoich funkcji.


1

Po pierwsze masz błąd w kodzie, który chcesz mieć Thing *thing(new Thing());i tylko return thing;.

  • Użyj shared_ptr<Thing>. Deref go, ponieważ był to wskaźnik. Zostanie on usunięty, gdy ostatnie odwołanie do Thingzawartego wyjdzie poza zakres.
  • Pierwsze rozwiązanie jest bardzo powszechne w bibliotekach naiwnych. Ma pewną wydajność i narzut składniowy, unikaj go, jeśli to możliwe
  • Użyj drugiego rozwiązania tylko wtedy, gdy możesz zagwarantować, że żadne wyjątki nie zostaną wyrzucone lub gdy wydajność jest absolutnie krytyczna (będziesz łączyć się z C lub assemblerem, zanim stanie się to istotne).

0

Jestem pewien, że ekspert C ++ przyjdzie z lepszą odpowiedzią, ale osobiście podoba mi się drugie podejście. Używanie inteligentnych wskaźników pomaga w rozwiązaniu problemu zapominania o deletei, jak mówisz, wygląda to lepiej niż konieczność tworzenia obiektu przed ręką (i nadal konieczność usuwania go, jeśli chcesz go przydzielić na stercie).

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.