Dlaczego zwracanie „wektora” z funkcji jest w porządku?


108

Proszę wziąć pod uwagę ten kod. Widziałem ten typ kodu kilka razy. wordsjest wektorem lokalnym. Jak można go zwrócić z funkcji?

Czy możemy zagwarantować, że nie umrze?

 std::vector<std::string> read_file(const std::string& path)
 {
    std::ifstream file("E:\\names.txt");

    if (!file.is_open())
    {
        std::cerr << "Unable to open file" << "\n";
        std::exit(-1);
    }

    std::vector<string> words;//this vector will be returned
    std::string token;

    while (std::getline(file, token, ','))
    {
        words.push_back(token);
    }

    return words;
}

18
Zostanie skopiowany po powrocie.
songyuanyao

6
Nikt nie gwarantuje .. To będzie umrzeć, ale po to skopiowane.
Maroun

7
Problem występuje tylko wtedy, gdy funkcja zwraca odwołanie:std::vector<std::string>&
Caduchon

14
@songyuanyao nie, zostanie przeniesiony.
prawy bok

15
@songyuanyao Yes. C ++ 11 to aktualny standard, więc C ++ 11 to C ++.
prawy bok

Odpowiedzi:


68

Czy możemy zagwarantować, że nie umrze?

Dopóki nie zostanie zwrócone żadne odniesienie, można to zrobić w porządku. wordszostanie przeniesiony do zmiennej otrzymującej wynik.

Zmienna lokalna wyjdzie poza zakres. po przeniesieniu (lub skopiowaniu).


2
Ale czy wydajne, lub czy istnieją jakiekolwiek obawy dotyczące wydajności, mówią o wektorze, który może pomieścić 1000 wpisów?
Zar

@zadane Czy to było pytanie? Wspomniałem również o przenoszeniu, które pozwoli uniknąć kopiowania wartości zwracanej w rzeczywistości (dostępne przynajmniej w obecnym standardzie).
πάντα ῥεῖ

2
Właściwie nie w pytaniu, ale szukałem odpowiedzi z tej perspektywy niezależnie. Nie wiem czy piszę pytanie, obawiam się, że zaznaczą to jako duplikat :)
zar

@zadane "Obawiam się, że oznaczy to jako duplikat tego" . Wystarczy spojrzeć na wyższą głosowaną odpowiedź . Nawet w przypadku starszych implementacji nie powinieneś się martwić, te i tak zostaną w większości poprawnie zoptymalizowane przez te kompilatory.
πάντα ῥεῖ

107

Przed C ++ 11:

Funkcja nie zwróci zmiennej lokalnej, ale raczej jej kopię. Twój kompilator może jednak przeprowadzić optymalizację, w której nie jest wykonywana żadna czynność kopiowania.

Zobacz to pytanie i odpowiedź, aby uzyskać więcej informacji.

C ++ 11:

Funkcja przesunie wartość. Więcej szczegółów znajdziesz w tej odpowiedzi .


2
Zostanie przeniesiony, a nie skopiowany. To jest gwarantowane.
prawy bok

1
Czy dotyczy to również C ++ 10?
Tim Meyer

28
Nie ma czegoś takiego jak C ++ 10.
prawy bok

C ++ 03 nie miał semantyki przenoszenia (ale kopiowanie mogło zostać usunięte), ale C ++ to C ++ 11, a pytanie dotyczyło C ++.
prawy bok

19
Istnieje oddzielny tag dla pytań występujących wyłącznie w C ++ 11. Wielu z nas, szczególnie programiści w większych firmach, wciąż jest skazanych na kompilatory, które jeszcze nie w pełni obsługują C ++ 11. Zaktualizowałem pytanie, aby było poprawne dla obu standardów.
Tim Meyer

26

Myślę, że odnosisz się do problemu w C (i C ++), że zwracanie tablicy z funkcji jest niedozwolone (lub przynajmniej nie będzie działać zgodnie z oczekiwaniami) - dzieje się tak, ponieważ tablica zwraca (jeśli zapiszesz ją w prosta forma) zwracają wskaźnik do rzeczywistej tablicy na stosie, która jest następnie natychmiast usuwana po powrocie funkcji.

Ale w tym przypadku działa, ponieważ std::vectorjest klasą, a klasy, podobnie jak struktury, mogą (i będą) kopiowane do kontekstu wywołującego. [Właściwie większość kompilatorów optymalizuje ten konkretny typ kopii za pomocą czegoś, co nazywa się „Optymalizacją wartości zwracanej”, wprowadzonej specjalnie w celu uniknięcia kopiowania dużych obiektów zwracanych z funkcji, ale jest to optymalizacja, az punktu widzenia programistów będzie to zachowują się tak, jakby konstruktor przypisania został wywołany dla obiektu]

Dopóki nie zwrócisz wskaźnika lub odniesienia do czegoś, co znajduje się w powracającej funkcji, wszystko jest w porządku.


13

Aby dobrze zrozumieć zachowanie, możesz uruchomić ten kod:

#include <iostream>

class MyClass
{
  public:
    MyClass() { std::cout << "run constructor MyClass::MyClass()" << std::endl; }
    ~MyClass() { std::cout << "run destructor MyClass::~MyClass()" << std::endl; }
    MyClass(const MyClass& x) { std::cout << "run copy constructor MyClass::MyClass(const MyClass&)" << std::endl; }
    MyClass& operator = (const MyClass& x) { std::cout << "run assignation MyClass::operator=(const MyClass&)" << std::endl; }
};

MyClass my_function()
{
  std::cout << "run my_function()" << std::endl;
  MyClass a;
  std::cout << "my_function is going to return a..." << std::endl;
  return a;
}

int main(int argc, char** argv)
{
  MyClass b = my_function();

  MyClass c;
  c = my_function();

  return 0;
}

Wynik jest następujący:

run my_function()
run constructor MyClass::MyClass()
my_function is going to return a...
run constructor MyClass::MyClass()
run my_function()
run constructor MyClass::MyClass()
my_function is going to return a...
run assignation MyClass::operator=(const MyClass&)
run destructor MyClass::~MyClass()
run destructor MyClass::~MyClass()
run destructor MyClass::~MyClass()

Zauważ, że ten przykład został dostarczony w kontekście C ++ 03, można go ulepszyć dla C ++> = 11


1
Ten przykład byłby bardziej kompletny, gdyby zawierał również konstruktor przenoszenia i operator przypisania przenoszenia, a nie tylko konstruktor kopiujący i operator przypisania kopiowania. (Jeśli funkcje przenoszenia nie są obecne, zamiast nich zostaną użyte funkcje kopiowania).
Some Guy

@SomeGuy Zgadzam się, ale nie używam C ++ 11. Nie mogę dostarczyć wiedzy, której nie mam. Dodaję notatkę. Zapraszam do dodania odpowiedzi dla C ++> = 11. :-)
Caduchon

-5

Nie wyrażam zgody i nie polecam zwrotu vector:

vector <double> vectorial(vector <double> a, vector <double> b)
{
    vector <double> c{ a[1] * b[2] - b[1] * a[2], -a[0] * b[2] + b[0] * a[2], a[0] * b[1] - b[0] * a[1] };
    return c;
}

To jest dużo szybsze:

void vectorial(vector <double> a, vector <double> b, vector <double> &c)
{
    c[0] = a[1] * b[2] - b[1] * a[2]; c[1] = -a[0] * b[2] + b[0] * a[2]; c[2] = a[0] * b[1] - b[0] * a[1];
}

Testowałem w programie Visual Studio 2017 z następującymi wynikami w trybie wydania:

8.01 MOP-ów przez odniesienie
5.09 MOPs powrotny wektor

W trybie debugowania jest znacznie gorzej:

0,053 MOPS przez odniesienie
0,034 MOPs przez wektor powrotu


-10

W rzeczywistości jest to błąd projektu. Nie powinieneś używać wartości zwracanej do niczego, co nie jest prymitywem, do niczego, co nie jest względnie trywialne.

Idealne rozwiązanie powinno zostać zaimplementowane poprzez parametr zwrotny z decyzją o referencji / wskaźniku i właściwym użyciu „const \ 'y \' ness” jako deskryptora.

Ponadto należy zdać sobie sprawę, że etykieta tablicy w C i C ++ jest w rzeczywistości wskaźnikiem, a jej subskrypcja jest w rzeczywistości symbolem przesunięcia lub dodania.

Tak więc etykieta lub ptr array_ptr === etykieta tablicy zwracająca foo [offset] tak naprawdę mówi zwracać element w miejscu wskaźnika pamięci foo + offset typu return type.


5
..........co. Wydaje się jasne, że nie masz kwalifikacji do rzucania oskarżeń typu „porażka projektu”. I rzeczywiście, promowanie semantyki wartości przez operacje RVO i ruch jest jednym z głównych sukcesów es nowoczesnego stylu C ++. Ale wydaje się, że utknąłeś myśląc o surowych tablicach i wskaźnikach, więc nie spodziewałbym się, że to zrozumiesz.
underscore_d
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.