Czy sensowne byłoby używanie obiektów (zamiast typów pierwotnych) do wszystkiego w C ++?

17

Podczas ostatniego projektu, nad którym pracowałem, musiałem użyć wielu funkcji, które wyglądają tak:

static bool getGPS(double plane_latitude, double plane_longitude, double plane_altitude,
                       double plane_roll, double plane_pitch, double plane_heading,
                       double gimbal_roll, double gimbal_pitch, double gimbal_yaw, 
                       int target_x, int target_y, double zoom,
                       int image_width_pixels, int image_height_pixels,
                       double & Target_Latitude, double & Target_Longitude, double & Target_Height);

Chcę go zmienić tak, aby wyglądał mniej więcej tak:

static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
                            PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)

Wydaje mi się, że jest znacznie bardziej czytelny i bezpieczny niż pierwsza metoda. Ale czy tworzenie PixelCoordinatei PixelSizeklasy mają sens ? A może lepiej byłoby po prostu użyć std::pair<int,int>dla każdego. I czy warto mieć ZoomLevelklasę, czy powinienem po prostu użyć double?

Moja intuicja dotycząca używania klas do wszystkiego opiera się na następujących założeniach:

  1. Jeśli istnieją klasy dla wszystkiego, nie byłoby możliwe podanie ZoomLevelw miejscu, w którym Weightoczekiwany był obiekt, więc trudniej byłoby podać niewłaściwe argumenty funkcji
  2. Podobnie, niektóre nielegalne operacje spowodowałyby błędy kompilacji, takie jak dodanie a GPSCoordinatedo ZoomLevelinnegoGPSCoordinate
  3. Czynności prawne będą łatwe do reprezentowania i bezpieczne. tzn. odjęcie dwóch GPSCoordinates dałoby aGPSDisplacement

Jednak większość kodu C ++, który widziałem, używa wielu prymitywnych typów i myślę, że musi to mieć dobry powód. Czy warto używać obiektów do czegokolwiek, czy ma wady, których nie jestem świadomy?

zackg
źródło
8
„większość kodu C ++, jaki widziałem, wykorzystuje wiele prymitywnych typów” - przychodzą mi na myśl dwie myśli: duża część kodu C ++ mogła zostać napisana przez programistów znających C, który ma słabszą abstrakcję; także Prawo
Jesiotra
3
I myślę, że to dlatego, że prymitywne typy są proste, proste, szybkie, eleganckie i wydajne. Teraz w przykładzie PO zdecydowanie dobrym pomysłem jest wprowadzenie nowych klas. Ale odpowiedź na pytanie tytułowe (gdzie mówi „wszystko”) brzmi: nie!
Pan Lister
1
@Max typedeffing the double to absolutnie nic nie rozwiązuje problemu OP.
Pan Lister
1
@CongXu: Myślałem prawie tak samo, ale bardziej w tym sensie, że „dużo kodu w dowolnym języku mogło zostać napisane przez programistów mających problemy z budowaniem abstrakcji”.
Doc Brown
1
Jak mówi przysłowie: „jeśli masz funkcję z 17 parametrami, prawdopodobnie przegapiłeś kilka”.
Joris Timmermans

Odpowiedzi:

24

Tak, zdecydowanie. Funkcje / metody, które pobierają zbyt wiele argumentów, mają zapach kodu i wskazują co najmniej jedną z następujących czynności:

  1. Funkcja / metoda robi zbyt wiele rzeczy na raz
  2. Funkcja / metoda wymaga dostępu do tak wielu rzeczy, ponieważ pyta, nie mówi ani nie narusza niektórych praw projektowych OO
  3. Argumenty są faktycznie ściśle powiązane

Jeśli tak jest w przypadku ostatniego (a twój przykład na pewno to sugeruje), najwyższy czas zrobić trochę tej abstrakcyjnej fantazji, o której wspominali fajne dzieciaki kilka dekad temu. W rzeczywistości poszedłbym dalej niż twój przykład i zrobiłbym coś takiego:

static GPSCoordinate getGPS(Plane plane, Angle3D gimbalAngle, GPSView view)

(Nie znam GPS, więc prawdopodobnie nie jest to poprawna abstrakcja; na przykład nie rozumiem, co zoom ma wspólnego z wywoływaną funkcją getGPS).

Proszę wolą zajęcia jak PixelCoordinatepowyżej std::pair<T, T>, nawet jeśli oba mają te same dane. Dodaje wartość semantyczną oraz odrobinę dodatkowego bezpieczeństwa typu (kompilator powstrzyma cię od przekazania ScreenCoordinatea, PixelCoordinatenawet jeśli oba są pairs.

congusbongus
źródło
1
nie wspominając o tym, że można przekazać większe struktury przez odniesienie do drobnej mikrooptymalizacji, którą wszystkie fajne dzieci starają się zrobić, ale naprawdę nie powinny
ratchet maniak
6

Uwaga: Wolę C niż C ++

Zamiast klas używałbym struktur. Ta kwestia jest co najwyżej pedantyczna, ponieważ struktury i klasy są prawie identyczne (zobacz tutaj prosty wykres lub to pytanie SO ), ale myślę, że różnicowanie ma sens.

Programiści C znają struktury jako bloki danych, które mają większe znaczenie po zgrupowaniu, natomiast gdy widzę klasy, zakładam, że istnieje pewien stan wewnętrzny i metody manipulowania tym stanem. Współrzędna GPS nie ma stanu, tylko dane.

Z twojego pytania wynika, że ​​jest to dokładnie to, czego szukasz, ogólny kontener, a nie struktura stanowa. Jak wspomnieli inni, nie zawracałbym sobie głowy umieszczeniem Zoomu we własnej klasie; Ja osobiście nienawidzę klas zbudowanych na typ danych (moi profesorowie uwielbiają tworzyć Heighti Idklasy).

Jeśli istnieją klasy dla wszystkiego, niemożliwe byłoby przekazanie ZoomLevel w miejscu, w którym oczekiwano obiektu Weight, dlatego trudniej byłoby podać nieprawidłowe argumenty funkcji

Nie sądzę, że to jest problem. Jeśli zmniejszysz liczbę parametrów poprzez pogrupowanie oczywistych rzeczy, które zrobiłeś, nagłówki powinny wystarczyć, aby zapobiec tym problemom. Jeśli naprawdę się martwisz, oddziel prymitywy strukturą, która powinna dać ci błędy kompilacji dla typowych błędów. Łatwo jest zamienić dwa parametry obok siebie, ale trudniej, jeśli są bardziej oddalone od siebie.

Podobnie niektóre nielegalne operacje mogą powodować błędy kompilacji, takie jak dodanie współrzędnej GPS do ZoomLevel lub innej współrzędnej GPS

Dobre wykorzystanie przeciążeń operatora.

Czynności prawne będą łatwe do reprezentowania i bezpieczne. tzn. odjęcie dwóch współrzędnych GPS dałoby przesunięcie GPS

Również dobre wykorzystanie przeciążeń operatora.

Myślę, że jesteś na dobrej drodze. Inną rzeczą do rozważenia jest to, jak pasuje to do reszty programu.

beatgammit
źródło
3

Korzystanie z typów dedykowanych ma tę zaletę, że znacznie trudniej jest sprecyzować kolejność argumentów. Na przykład pierwsza wersja przykładowej funkcji zaczyna się łańcuchem 9 podwójnych. Gdy ktoś korzysta z tej funkcji, bardzo łatwo jest zepsuć kolejność i podać kąty gimbala zamiast współrzędnych GPS i odwrotnie. Może to prowadzić do bardzo dziwnych i trudnych do wyśledzenia błędów. Jeśli zamiast tego przekażesz tylko trzy argumenty różnych typów, ten błąd może zostać przechwycony przez kompilator.

W rzeczywistości rozważę pójście o krok dalej i wprowadzenie typów, które są technicznie identyczne, ale mają inne znaczenie semantyczne. Na przykład posiadając klasę PlaneAngle3di oddzielną klasę GimbalAngle3d, mimo że obie są zbiorem trzech podwójnych. Uniemożliwiłoby to użycie jednego jako drugiego, chyba że jest to celowe.

Poleciłbym użycie typedefs, które są tylko aliasami dla typów pierwotnych ( typedef double ZoomLevel) nie tylko z tego powodu, ale także z innego powodu: Pozwala to łatwo zmienić typ później. Kiedy pewnego dnia pojawi się pomysł, że używanie floatmoże być równie dobre, doubleale może być bardziej wydajne pod względem pamięci i procesora, wystarczy zmienić to w jednym miejscu, a nie w kilkudziesięciu.

Philipp
źródło
+1 za sugestię typedef. Zapewnia to, co najlepsze z obu światów: prymitywne typy i obiekty.
Karl Bielefeldt
Nie jest trudniej zmienić typ członka klasy niż typedef; czy miałeś na myśli w porównaniu do prymitywów?
MaHuJa
2

Świetne odpowiedzi już tu są, ale chciałbym dodać jedną rzecz:

Używanie PixelCoordinatei PixelSizeklasa czyni rzeczy bardzo specyficznymi. Generał Coordinatelub Point2Dklasa może lepiej pasować do twoich potrzeb. A Point2Dpowinna być klasą realizującą najczęstsze operacje punkt / wektor. Można realizować taką klasę nawet rodzajowo ( Point2D<T>gdzie T może być intlub doubleczy coś innego).

Operacje punkt / wektor 2D są tak bardzo powszechne w przypadku wielu zadań programowania geometrii 2D, że według mojego doświadczenia taka decyzja znacznie zwiększa szansę na ponowne wykorzystanie istniejących operacji 2D. Będzie można uniknąć konieczności ponownego wdrażania ich do siebie PixelCoordinate, GPSCoordinate„Brak jakichkolwiek zastrzeżeń” -Coordinate klasa znowu i znowu.

Doktor Brown
źródło
1
możesz także zrobić, struct PixelCoordinate:Point2D<int>jeśli chcesz odpowiedniego bezpieczeństwa typu
maniak zapadkowy
0

Chcę go zmienić tak, aby wyglądał mniej więcej tak:

static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
                        PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)

Wydaje mi się, że jest znacznie bardziej czytelny i bezpieczny niż pierwsza metoda.

Jest [bardziej czytelny]. To także dobry pomysł.

Ale czy warto tworzyć klasy PixelCoordinate i PixelSize?

Jeśli reprezentują różne koncepcje, ma to sens (to znaczy „tak”).

A może lepiej byłoby po prostu użyć std :: pair dla każdego.

Możesz zacząć od zrobienia typedef std::pair<int,int> PixelCoordinate, PixelSize;. W ten sposób pokrywasz semantykę (pracujesz ze współrzędnymi i rozmiarami pikseli, a nie ze std :: parami w kodzie klienta) i jeśli potrzebujesz rozszerzyć, po prostu zamieniasz typedef na klasę, a kod klienta powinien wymagają minimalnych zmian.

I czy warto mieć klasę ZoomLevel, czy powinienem użyć podwójnej?

Można go również wpisać, ale używałbym, doubledopóki nie potrzebowałbym powiązanej z nim funkcji, która nie istnieje dla podwójnego (na przykład posiadanie ZoomLevel::defaultwartości wymagałoby zdefiniowania klasy, ale dopóki nie będzie czegoś podobnego to podwójnie).

Moja intuicja dotycząca używania klas do wszystkiego opiera się na tych założeniach [pominiętych ze względu na zwięzłość]

Są to mocne argumenty (zawsze powinieneś dążyć do tego, aby twoje interfejsy były łatwe w użyciu i trudne w użyciu).

Jednak większość kodu C ++, który widziałem, używa wielu prymitywnych typów i myślę, że musi to mieć dobry powód.

Są powody; jednak w wielu przypadkach powody te nie są dobre. Jednym z ważnych powodów tego może być wydajność, ale to oznacza, że ​​powinieneś widzieć ten rodzaj kodu jako wyjątek, a nie z reguły.

Czy warto używać obiektów do czegokolwiek, czy ma wady, których nie jestem świadomy?

Zasadniczo dobrym pomysłem jest upewnienie się, że jeśli twoje wartości zawsze pojawiają się razem (i nie mają żadnego sensu osobno), powinieneś je pogrupować (z tych samych powodów, o których wspomniałeś w swoim poście).

Oznacza to, że jeśli masz współrzędne, które zawsze mają x, yiz, to znacznie lepiej jest mieć coordinateklasę niż przesyłać wszędzie trzy parametry.

utnapistim
źródło