Czy powinienem inicjować struktury C za pomocą parametru, czy wartości zwracanej? [Zamknięte]

33

Firma, w której pracuję, inicjuje wszystkie swoje struktury danych za pomocą funkcji inicjalizacji, takiej jak:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Dostałem trochę odpowiedzi, próbując zainicjować moje struktury w następujący sposób:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Czy istnieje przewaga jednej nad drugą?
Który powinienem zrobić i jak usprawiedliwić to lepszą praktyką?

Trevor Hickey
źródło
4
@gnat To jest wyraźne pytanie dotyczące inicjalizacji struktury. Wątek ten zawiera niektóre z tych samych uzasadnień, które chciałbym zastosować w przypadku tej konkretnej decyzji projektowej.
Trevor Hickey,
2
@Jefffrey Jesteśmy w C, więc nie możemy faktycznie mieć metod. Nie zawsze jest to także bezpośredni zestaw wartości. Czasami inicjowanie struktury ma na celu uzyskanie wartości (jakoś) i wykonuje pewną logikę w celu inicjalizacji struktury.
Trevor Hickey,
1
@JacquesB Mam: „Każdy komponent, który zbudujesz, będzie inny niż inne. Istnieje funkcja Initialize () używana w innym miejscu dla struktury. Mówiąc technicznie, nazywanie go konstruktorem wprowadza w błąd”.
Trevor Hickey,
1
@TrevorHickey InitializeFoo()jest konstruktorem. Jedyną różnicą w stosunku do konstruktora C ++ jest to, że thiswskaźnik jest przekazywany jawnie, a nie domyślnie. Skompilowany kod InitializeFoo()i odpowiadający mu C ++ Foo::Foo()jest dokładnie taki sam.
cmaster
2
Lepsza opcja: Przestań używać C zamiast C ++. Autowin.
Thomas Eding,

Odpowiedzi:

25

W drugim podejściu nigdy nie będziesz mieć w połowie zainicjowanego Foo. Umieszczenie całej konstrukcji w jednym miejscu wydaje się bardziej sensownym i oczywistym miejscem.

Ale ... pierwszy sposób nie jest taki zły i jest często używany w wielu obszarach (jest nawet dyskusja na temat najlepszego sposobu wstrzykiwania zależności, albo zastrzyku własności jak twój pierwszy sposób, lub zastrzyk konstruktora jak drugi) . Żaden też nie jest zły

Więc jeśli żadne z nich nie jest złe, a reszta firmy stosuje podejście nr 1, powinieneś dopasować się do istniejącej bazy kodów i nie próbować zepsuć go poprzez wprowadzenie nowego wzorca. To naprawdę najważniejszy czynnik w grze, graj fajnie ze swoimi nowymi przyjaciółmi i nie staraj się być tym wyjątkowym płatkiem śniegu, który robi wszystko inaczej.

gbjbaanb
źródło
Ok, wydaje się rozsądne. Miałem wrażenie, że inicjowanie obiektu bez możliwości zobaczenia, jaki typ danych inicjujących inicjuje, doprowadzi do zamieszania. Starałem się podążać za koncepcją wprowadzania / wyprowadzania danych, aby stworzyć przewidywalny i testowalny kod. Wykonanie tego w inny sposób wydawało się zwiększać sprzężenie, ponieważ plik źródłowy mojej struktury potrzebował dodatkowych zależności do przeprowadzenia inicjalizacji. Masz rację, ponieważ nie chcę kołysać łodzią, chyba że jeden sposób jest bardziej preferowany niż inny.
Trevor Hickey,
4
@TrevorHickey: Właściwie powiedziałbym, że istnieją dwie kluczowe różnice między podanymi przez ciebie przykładami - (1) W jednym funkcja jest przekazywana wskaźnikiem do struktury w celu zainicjowania, aw drugim zwraca zainicjowaną strukturę; (2) W jednym parametry inicjalizacji są przekazywane do funkcji, w drugim są one niejawne. Wygląda na to, że pytasz o więcej (2), ale odpowiedzi koncentrują się na (1). Możesz wyjaśnić, że - podejrzewam, że większość ludzi poleciłaby hybrydę tych dwóch przy użyciu wyraźnych parametrów i wskaźnika:void SetupFoo(Foo *out, int a, int b, int c)
psmears
1
W jaki sposób pierwsze podejście doprowadziłoby do struktury „w połowie zainicjalizowanej” Foo? Pierwsze podejście wykonuje również całą inicjalizację w jednym miejscu. (A może rozważa un zainicjowany Foostruct być „pół-zainicjowany”?)
jamesdlin
1
@jamesdlin w przypadkach, w których Foo jest tworzony, a InitialiseFoo jest przypadkowo pominięty. Opisywanie 2-fazowej inicjalizacji było tylko figurą mowy, bez długiego opisu. Doszedłem do wniosku, że doświadczeni deweloperzy zrozumieją.
gbjbaanb
22

Oba podejścia łączą kod inicjujący w jedno wywołanie funkcji. Jak na razie dobrze.

Istnieją jednak dwa problemy z drugim podejściem:

  1. Drugi nie konstruuje wynikowego obiektu, inicjuje inny obiekt na stosie, który jest następnie kopiowany do obiektu końcowego. Dlatego uważam drugie podejście za nieco gorsze. Odepchnięcie, które otrzymałeś, jest prawdopodobnie spowodowane tą obcą kopią.

    Jest jeszcze gorzej, gdy czerpią klasę Derivedz Foo(struktury są szeroko stosowane do orientacji obiektu w C): Przy drugim podejściu, funkcja ConstructDerived()nazwałbym ConstructFoo()skopiuj wynikowy tymczasowy Fooobiekt nad do gniazda nadklasą w Derivedobiekcie; dokończ inicjalizację Derivedobiektu; tylko po to, aby wynikowy obiekt był ponownie kopiowany po powrocie. Dodaj trzecią warstwę, a wszystko stanie się całkowicie śmieszne.

  2. W drugim podejściu ConstructClass()funkcje nie mają dostępu do adresu obiektu w budowie. Uniemożliwia to łączenie obiektów podczas budowy, ponieważ jest to konieczne, gdy obiekt musi zarejestrować się w innym obiekcie w celu wywołania zwrotnego.


Wreszcie, nie wszystkie structssą pełnoprawnymi klasami. Niektóre structsskutecznie łączą wiązkę zmiennych bez żadnych wewnętrznych ograniczeń wartości tych zmiennych. typedef struct Point { int x, y; } Point;byłby dobrym tego przykładem. W tych przypadkach użycie funkcji inicjalizującej wydaje się nadmierne. W takich przypadkach dosłowna składnia złożona może być wygodna (jest to C99):

Point = { .x = 7, .y = 9 };

lub

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}
cmaster
źródło
5
Nie sądziłem, że kopie będą problemem z powodu en.wikipedia.org/wiki/Copy_elision
Trevor Hickey
5
To, że kompilator może pominąć kopię, nie zmniejsza faktu, że zapisałeś kopię. W C pisanie zbędnych operacji i poleganie na kompilatorze w celu ich naprawy jest uważane za zły styl. Różni się to od C ++, gdzie ludzie są dumni, gdy mogą udowodnić, że kompilator może teoretycznie usunąć wszystkie cruft pozostawione przez ich zagnieżdżone szablony. W C ludzie próbują napisać dokładnie kod, który maszyna powinna wykonać. W każdym razie pozostaje kwestia niedostępności adresów, kopiowanie danych nie może ci w tym pomóc.
cmaster
3
Każdy, kto używa kompilatora, powinien oczekiwać, że kod, który piszą, zostanie przekształcony przez kompilator. O ile nie uruchomią sprzętowego interpretera języka C, kod, który piszą, nie będzie kodem, który wykonują, nawet jeśli łatwo jest uwierzyć inaczej. Jeśli zrozumieją swój kompilator, zrozumieją elision, i nie różni się niczym od int x = 3;przechowywania łańcucha xw pliku binarnym. Adres i punkty dziedziczenia są dobre; zakładane niepowodzenie elekcji jest głupie.
Yakk,
@Yakk: Historycznie, C został wymyślony, aby służyć jako forma asemblera wysokiego poziomu do programowania systemów. Od tego czasu jego tożsamość staje się coraz bardziej mroczna. Niektórzy ludzie chcą, aby był to zoptymalizowany język aplikacji, ale ponieważ nie pojawiła się lepsza forma języka asemblera na wysokim poziomie, C jest nadal potrzebne, aby spełniać tę ostatnią rolę. Nie widzę nic złego w pomyśle, że dobrze napisany kod programu powinien zachowywać się co najmniej przyzwoicie, nawet po skompilowaniu z minimalnymi optymalizacjami, choć aby to naprawdę działało, C musiałoby dodać rzeczy, których od dawna brakowało.
supercat
@Yakk: Na przykład posiadanie dyrektyw informujących kompilator „Następujące zmienne mogą być bezpiecznie przechowywane w rejestrach podczas następnego odcinka kodu”, a także sposób kopiowania bloków typu innego niż unsigned charumożliwiłby optymalizację, dla której Reguła ścisłego aliasingu byłaby niewystarczająca, a jednocześnie wyraźniejsze oczekiwania programistów.
supercat
1

W zależności od zawartości struktury i konkretnego używanego kompilatora, każde podejście może być szybsze. Typowy wzorzec jest taki, że struktury spełniające określone kryteria mogą zostać zwrócone do rejestrów; w przypadku funkcji zwracających inne typy struktur osoba wywołująca jest zobowiązana do przydzielenia miejsca dla tymczasowej struktury gdzieś (zwykle na stosie) i przekazania jej adresu jako parametru „ukrytego”; w przypadkach, gdy zwrot funkcji jest przechowywany bezpośrednio w zmiennej lokalnej, której adres nie jest przechowywany przez żaden kod zewnętrzny, niektóre kompilatory mogą być w stanie przekazać adres tej zmiennej bezpośrednio.

Jeśli typ struktury spełnia określone wymagania implementacji, które mają zostać zwrócone do rejestrów (np. Nie mogą być większe niż jedno słowo maszynowe lub wypełnić dokładnie dwa słowa maszynowe) posiadające funkcję return, struktura może być szybsza niż przekazanie adresu struktury, szczególnie ponieważ udostępnienie adresu zmiennej zewnętrznemu kodowi, który może przechowywać jego kopię, może wykluczyć niektóre przydatne optymalizacje. Jeśli typ nie spełnia takich wymagań, wygenerowany kod dla funkcji zwracającej strukturę będzie podobny do kodu dla funkcji, która akceptuje wskaźnik docelowy; kod wywołujący byłby prawdopodobnie szybszy dla formularza, który przyjmuje wskaźnik, ale ten formularz traci pewne możliwości optymalizacji.

Szkoda, że ​​C nie pozwala na stwierdzenie, że funkcja nie może przechowywać kopii przekazanego wskaźnika (semantyka podobna do odwołania do C ++), ponieważ przekazanie takiego ograniczonego wskaźnika zyskałoby bezpośrednie korzyści z przekazania wskaźnik do wcześniej istniejącego obiektu, ale jednocześnie unikaj semantycznych kosztów wymagających od kompilatora uznania adresu zmiennej za „odsłonięty”.

supercat
źródło
3
Do ostatniego punktu: w C ++ nie ma nic, co powstrzymałoby funkcję przed zachowaniem kopii wskaźnika przekazanego jako odwołanie, funkcja może po prostu pobrać adres obiektu. Można również użyć odwołania do skonstruowania innego obiektu zawierającego to odwołanie (nie tworzy się goły wskaźnik). Niemniej jednak kopia wskaźnika lub odwołanie w obiekcie może przeżyć obiekt, na który wskazują, tworząc zwisający wskaźnik / odwołanie. Zatem kwestia bezpieczeństwa odniesienia jest dość niemowa.
cmaster
@cmaster: Na platformach, które zwracają struktury poprzez przekazanie wskaźnika do pamięci tymczasowej, kompilatory C nie zapewniają wywoływanych funkcji w żaden sposób dostępu do adresu tej pamięci. W C ++ możliwe jest uzyskanie adresu zmiennej przekazywanej przez referencję, ale jeśli wywołujący nie zagwarantuje trwałości przekazanego elementu (w takim przypadku zwykle przeszedłby wskaźnik), prawdopodobnie wystąpi niezdefiniowane zachowanie.
supercat
1

Jednym argumentem na korzyść stylu „parametr wyjściowy” jest to, że pozwala on funkcji zwrócić kod błędu.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

Biorąc pod uwagę pewien zestaw pokrewnych struktur, jeśli inicjalizacja może się nie powieść dla któregokolwiek z nich, warto, aby ze względu na spójność wszystkie z nich używały stylu outparametrowego.

Viktor Dahl
źródło
1
Chociaż można po prostu ustawić errno .
Deduplicator
0

Zakładam, że skupiasz się na inicjalizacji za pomocą parametru wyjściowego vs. inicjalizacji przez powrót, a nie na rozbieżności w dostarczaniu argumentów konstrukcyjnych.

Zauważ, że pierwsze podejście może Foobyć nieprzejrzyste (choć nie tak, jak obecnie go używasz), i jest to zwykle pożądane ze względu na długoterminową łatwość konserwacji. Można na przykład rozważyć funkcję, która przydziela nieprzezroczystą Foostrukturę bez jej inicjowania. A może trzeba ponownie zainicjować Foostrukturę, która została wcześniej zainicjowana przy użyciu różnych wartości.

jamesdlin
źródło
Downvoter, chcesz wyjaśnić? Czy coś, co powiedziałem, jest niepoprawne?
jamesdlin