Pochodzę z java tle, ale teraz pracuję na dużych bazach kodu C++. Często widzę ten wzór:

void function(int value, int& result);

A powyższa metoda nazywa się tak:

int result = 0;
function(42, result);
std::cout << "Result is " << result << std::endl;

W javie bardziej powszechne byłyby:

int result = function(42);

Chociaż powyższe jest całkowicie możliwe w C++, dlaczego to pierwsze pojawia się częściej (przynajmniej w kodzie, nad którym pracuję)? Czy to stylistyczne, czy coś więcej?

c++
1
imrichardcole 18 czerwiec 2021, 15:38

4 odpowiedzi

Najlepsza odpowiedź

Po pierwsze, dawniej była to uznana technika, aby mieć więcej niż jedno wyjście funkcji. Na przykład. w tym podpisie,

int computeNumberButMightFail(int& error_code);

Miałbyś zarówno ładunek int jako wartość zwracaną, jak i odwołanie do jakiejś zmiennej błędu, która jest ustawiana z poziomu funkcji, aby zasygnalizować błąd. W dzisiejszych czasach wiadomo, że istnieją lepsze techniki, m.in. std::optional<T> to dobra wartość zwracana, może istnieć bardziej elastyczny std::expected<T, ...>, a przy nowszych standardach C++ możemy zwrócić wiele wartości za pomocą std::make_tuple i zdestrukturyzować je po stronie wywołania za pomocą powiązań strukturalnych . W przypadku wyjątkowych scenariuszy błędów zwykle stosuje się… no cóż… wyjątki.

Po drugie, jest to technika optymalizacji z czasów, w których (N)RVO nie było powszechnie dostępne: jeśli wyjściem funkcji jest obiekt, którego kopiowanie jest drogie, chciałeś się upewnić, że nie są tworzone żadne niepotrzebne kopie:

void fillThisHugeBuffer(std::vector<LargeType>& output);

Oznacza, że ​​przekazujemy referencję do danych, aby uniknąć niepotrzebnej kopii podczas zwracania jej według wartości. Jednak jest to również przestarzałe, a zwracanie dużych obiektów według wartości jest zwykle uważane za bardziej idiomatyczne podejście, ponieważ C++17 gwarantuje coś, co nazywa się materializacją tymczasowych elementów tymczasowych, a optymalizacja zwracanej wartości (nazwa) jest implementowana przez wszystkie główne kompilatory.

Zobacz także podstawowe wytyczne: F.20 — „Dla „out” wartości wyjściowych preferuj wartości zwracane parametry wyjściowe".

4
lubgr 18 czerwiec 2021, 12:51

O ile wiem, ten przypadek nie jest powszechny w C++, przynajmniej nie z prymitywnymi typami danych jako wartościami zwracanymi. Należy wziąć pod uwagę kilka przypadków:

  1. Jeśli pracujesz ze zwykłym C lub w bardzo ograniczonym kontekście, gdzie wyjątki C++ są niedozwolone (jak aplikacje czasu rzeczywistego). Wtedy wartość zwracana przez funkcję jest często używana do wskazania sukcesu funkcji. A w C może być:
#include <stdio.h>
#include <errno.h>

int func(int arg, int* res) {
  if(arg > 10) {
    return EINVAL; //this is an error code from errnoe
  }
  
  ... //do stuff
  *res = my_result;
}

Jest to czasami używane również w C++, więc wynik musi być przypisany przez odwołanie/wskaźnik.

  1. Gdy wynikiem jest struktura lub obiekt, który istnieje przed wywołaniem funkcji, a celem funkcji jest modyfikacja atrybutów wewnątrz struktury lub obiektu. Jest to powszechny wzorzec, ponieważ i tak musisz przekazać argument przez odwołanie (aby uniknąć kopii). Nie jest więc konieczne zwracanie tego samego obiektu, który przekazujesz do funkcji. Przykładem w C++ może być:
#include <iostream>

struct Point {
  int x = 0;
  int y = 0;
};

void fill_point(Point& p, int x, int y) {
   p.x = x;
   p.y = y;
}

int main() {
  Point p();
  fill_point(p);

  return EXIT_SUCCESS;
}

Jest to jednak trywialne i istnieją lepsze rozwiązania, takie jak zdefiniowanie funkcji fill jako metody w obiekcie. Czasami jednak w odniesieniu do paradygmatu obiektów, na których spoczywa pojedyncza odpowiedzialność, wzorzec ten jest powszechny w bardziej złożonych okolicznościach.

  1. W Javie nie możesz kontrolować swojej sterty. Każdy zdefiniowany obiekt znajduje się na stercie i jest automatycznie przekazywany przez odwołanie do funkcji. W C++ masz wybór, gdzie nie chcesz przechowywać swojego obiektu (sterta lub stos) i jak przekazać obiekt do funkcji. Należy pamiętać, że przekazanie wartości obiektu kopiuje go, a zwrócenie obiektu z funkcji przez wartość również kopiuje obiekt. Aby zwrócić obiekt przez referencję, musisz upewnić się, że jego cykl życia przekracza zakres twojej funkcji, umieszczając go na stercie lub przekazując do funkcji przez referencję.
1
Alexander Fratzer 18 czerwiec 2021, 13:04

Modyfikowalne parametry, które otrzymują wartości jako efekt uboczny wywołania funkcji, są nazywane parametrami out. Są one ogólnie akceptowane jako nieco archaiczne i wyszły nieco z mody, ponieważ lepsze techniki są dostępne w C++. Jak sugerowałeś, zwrócenie wartości obliczonych z funkcji jest idealne.

Ale ograniczenia w świecie rzeczywistym czasami kierują ludzi w stronę parametrów:

  1. zwracanie obiektów po wartości jest zbyt drogie ze względu na koszt kopiowania dużych obiektów lub tych z nietrywialnymi konstruktorami kopiującymi

  2. zwracanie wielu wartości i tworzenie krotki lub struktury, która je zawiera, jest niewygodne, drogie lub niemożliwe.

  3. Gdy obiekty nie można skopiować (możliwe, że prywatny lub usunięty konstruktor kopiujący), ale muszą zostać utworzone „na miejscu”

Większość z tych problemów napotyka na przestarzały kod, ponieważ C++11 zyskał „semantykę przenoszenia”, a C++17 zyskał „gwarantowaną eliminację kopii”, co omija większość tych przypadków.

W każdym nowym kodzie używanie parametrów jest zwykle uważane za zły styl lub zapach kodu i najprawdopodobniej jest to nabyty nawyk, który przeniósł się z przeszłości (kiedy była to bardziej odpowiednia technika). źle, ale jedną z tych rzeczy, których staramy się unikać, jeśli nie jest to absolutnie konieczne.

1
Chris Uzdavinis 18 czerwiec 2021, 13:11

Istnieje kilka powodów, dla których parametr out może być używany w bazie kodu C++.

Na przykład:

  • Masz wiele wyjść:

    void compute(int a, int b, int &x, int &y) { x=a+b; y=a-b; }

  • Potrzebujesz wartości zwracanej do czegoś innego: Na przykład podczas parsowania PEG możesz znaleźć coś takiego:

    if (parseSymbol(pos,symbolName) && parseToken(pos,"=") && parseExpression(pos,exprNode)) {...}

Gdzie wyglądają funkcje parsowania

bool parseSymbol(int &pos, string &symbolName); 
bool parseToken(int &pos, const char *token); 

I tak dalej.

  • Aby uniknąć kopii obiektów.

  • Programista nie wiedział lepiej.

Ale w zasadzie myślę, że każda odpowiedź jest oparta na opiniach, ponieważ to kwestia stylu i zasad kodowania, czy i jak są używane parametry, czy nie.

0
Nordic Mainframe 18 czerwiec 2021, 12:54