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ą?
c
coding-style
constructors
initialization
Trevor Hickey
źródło
źródło
InitializeFoo()
jest konstruktorem. Jedyną różnicą w stosunku do konstruktora C ++ jest to, żethis
wskaźnik jest przekazywany jawnie, a nie domyślnie. Skompilowany kodInitializeFoo()
i odpowiadający mu C ++Foo::Foo()
jest dokładnie taki sam.Odpowiedzi:
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.
źródło
void SetupFoo(Foo *out, int a, int b, int c)
Foo
? Pierwsze podejście wykonuje również całą inicjalizację w jednym miejscu. (A może rozważa un zainicjowanyFoo
struct być „pół-zainicjowany”?)Oba podejścia łączą kod inicjujący w jedno wywołanie funkcji. Jak na razie dobrze.
Istnieją jednak dwa problemy z drugim podejściem:
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ę
Derived
zFoo
(struktury są szeroko stosowane do orientacji obiektu w C): Przy drugim podejściu, funkcjaConstructDerived()
nazwałbymConstructFoo()
skopiuj wynikowy tymczasowyFoo
obiekt nad do gniazda nadklasą wDerived
obiekcie; dokończ inicjalizacjęDerived
obiektu; tylko po to, aby wynikowy obiekt był ponownie kopiowany po powrocie. Dodaj trzecią warstwę, a wszystko stanie się całkowicie śmieszne.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
structs
są pełnoprawnymi klasami. Niektórestructs
skutecznie łą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):lub
źródło
int x = 3;
przechowywania łańcuchax
w pliku binarnym. Adres i punkty dziedziczenia są dobre; zakładane niepowodzenie elekcji jest głupie.unsigned char
umożliwiłby optymalizację, dla której Reguła ścisłego aliasingu byłaby niewystarczająca, a jednocześnie wyraźniejsze oczekiwania programistów.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”.
źródło
Jednym argumentem na korzyść stylu „parametr wyjściowy” jest to, że pozwala on funkcji zwrócić kod błędu.
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.
źródło
errno
.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
Foo
być 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ąFoo
strukturę bez jej inicjowania. A może trzeba ponownie zainicjowaćFoo
strukturę, która została wcześniej zainicjowana przy użyciu różnych wartości.źródło