Avrebbe senso usare oggetti (anziché tipi primitivi) per tutto in C ++?


17

Durante un recente progetto a cui ho lavorato, ho dovuto usare molte funzioni che assomigliano a questo:

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);

Quindi voglio riformattare in modo che assomigli a questo:

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

Questo mi sembra significativamente più leggibile e sicuro rispetto al primo metodo. Ma ha senso creare PixelCoordinatee PixelSizeclassi? O starei meglio usando solo std::pair<int,int>per ognuno. E ha senso tenere una ZoomLevellezione, o dovrei semplicemente usare una double?

La mia intuizione dietro l'utilizzo delle classi per tutto si basa su questi presupposti:

  1. Se ci sono classi per tutto, sarebbe impossibile passare un punto ZoomLevelin cui Weightera previsto un oggetto, quindi sarebbe più difficile fornire gli argomenti sbagliati a una funzione
  2. Allo stesso modo, alcune operazioni illegali causerebbero errori di compilazione, come l'aggiunta di una GPSCoordinatea ZoomLevelo un'altraGPSCoordinate
  3. Le operazioni legali saranno facili da rappresentare e da scrivere. cioè sottraendo due GPSCoordinates si otterrebbe aGPSDisplacement

Tuttavia, la maggior parte del codice C ++ che ho visto utilizza molti tipi primitivi e immagino che ci debba essere una buona ragione per questo. È una buona idea usare oggetti per qualcosa o ha degli aspetti negativi di cui non sono a conoscenza?


8
"la maggior parte del codice C ++ che ho visto usa molti tipi primitivi" - mi vengono in mente due pensieri: un sacco di codice C ++ potrebbe essere stato scritto da programmatori che hanno familiarità con C che ha un'astrazione più debole; anche Sturgeon's Law
congusbongus,

3
E penso che sia perché i tipi primitivi sono semplici, diretti, veloci, eleganti ed efficienti. Ora nell'esempio del PO, è sicuramente una buona idea introdurre alcune nuove classi. Ma la risposta alla domanda del titolo (dove dice "tutto") è un clamoroso no!
Sig. Lister,

1
@Max digitando il doppio non fa assolutamente nulla per risolvere il problema del PO.
Sig. Lister,

1
@CongXu: avevo quasi lo stesso pensiero, ma più nel senso "un sacco di codice in qualsiasi lingua potrebbe essere stato scritto da programmatori che hanno problemi con la costruzione di astrazioni".
Doc Brown,

1
Come dice il proverbio "se hai una funzione con 17 parametri, probabilmente ne hai persi alcuni".
Joris Timmermans,

Risposte:


24

Sì, sicuramente. Funzioni / metodi che accettano troppi argomenti sono odori di codice e indicano almeno uno dei seguenti:

  1. La funzione / metodo sta facendo troppe cose contemporaneamente
  2. La funzione / metodo richiede l'accesso a molte cose perché chiede, non dice o viola alcune leggi di progettazione OO
  3. Gli argomenti sono in realtà strettamente correlati

Se l'ultimo è il caso (e il tuo esempio lo suggerisce sicuramente), è giunto il momento di fare un po 'di quella "astrazione" di quei pantaloni fantasiosi di cui i ragazzi fantastici stavano parlando oh, solo alcuni decenni fa. In effetti, andrei oltre il tuo esempio e farei qualcosa del tipo:

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

(Non ho familiarità con il GPS, quindi questa probabilmente non è un'astrazione corretta; ad esempio, non capisco cosa abbia a che fare lo zoom con una funzione chiamata getGPS.)

Preferisci le classi come PixelCoordinatesopra std::pair<T, T>, anche se i due hanno gli stessi dati. Aggiunge valore semantico, oltre a un po 'di sicurezza in più di tipo (il compilatore ti impedirà di passare da ScreenCoordinatea a PixelCoordinateanche se entrambi sono pairs.


1
per non parlare del fatto che puoi superare le strutture più grandi facendo riferimento a quel po 'di micro-ottimizzazione che tutti i ragazzi fantastici stanno cercando di fare ma davvero non dovrebbero
maniaco del cricchetto

6

Disclaimer: preferisco il C al C ++

Invece delle lezioni, userei le strutture. Nella migliore delle ipotesi questo punto è pedante, poiché le strutture e le classi sono quasi identiche (vedi qui per un grafico semplice o questa domanda SO ), ma penso che ci sia un merito nella differenziazione.

I programmatori C hanno familiarità con le strutture come blocchi di dati che hanno un significato maggiore quando raggruppati, mentre quando vedo una clasas, presumo che ci sia uno stato interno e metodi per manipolarlo. Una coordinata GPS non ha stato, solo dati.

Dalla tua domanda, sembra che questo sia esattamente quello che stai cercando, un contenitore generale, non una struttura con stato. Come hanno già detto gli altri, non mi preoccuperei di mettere Zoom in una classe a sé stante; Personalmente odio le classi create per contenere un tipo di dati (i miei professori adorano creare Heighte Idclassi).

Se ci sono classi per tutto, sarebbe impossibile passare uno ZoomLevel in cui era previsto un oggetto Weight, quindi sarebbe più difficile fornire gli argomenti sbagliati a una funzione

Non penso che questo sia un problema. Se riduci il numero di parametri raggruppando le cose ovvie come hai fatto, le intestazioni dovrebbero essere sufficienti per prevenire questi problemi. Se sei davvero preoccupato, separa le primitive con una struttura, che dovrebbe darti errori di compilazione per errori comuni. È facile scambiare due parametri uno accanto all'altro, ma più difficile se sono più distanti.

Allo stesso modo, alcune operazioni illegali causerebbero errori di compilazione, come l'aggiunta di un GPS Coordinate a ZoomLevel o un altro GPS Coordinate

Buon uso dei sovraccarichi dell'operatore.

Le operazioni legali saranno facili da rappresentare e da scrivere. vale a dire che sottraendo due coordinate GPS si otterrebbe uno spostamento GPS

Anche un buon uso dei sovraccarichi dell'operatore.

Penso che tu sia sulla strada giusta. Un'altra cosa da considerare è come questo si adatta al resto del programma.


3

L'uso di tipi dedicati ha il vantaggio che è molto più difficile rovinare l'ordine degli argomenti. La prima versione della funzione di esempio, ad esempio, inizia con una catena di 9 doppie. Quando qualcuno usa questa funzione, è molto facile rovinare l'ordine e passare gli angoli del gimbal al posto delle coordinate GPS e viceversa. Questo può portare a bug molto strani e difficili da rintracciare. Quando si passano invece solo tre argomenti con tipi diversi, questo compilatore può rilevare questo errore.

Considererei infatti di fare un ulteriore passo avanti e introdurre tipi tecnicamente identici ma con un diverso significato semantico. Ad esempio, avendo una classe PlaneAngle3de una classe separata GimbalAngle3d, sebbene entrambe siano una raccolta di tre doppie. Ciò renderebbe impossibile utilizzare l'uno come l'altro, a meno che non sia intenzionale.

Consiglierei di usare i typedef che sono solo alias per tipi primitivi ( typedef double ZoomLevel) non solo per quel motivo, ma anche per un altro: ti permette facilmente di cambiare il tipo in seguito. Quando un giorno ti viene l'idea che l'utilizzo floatpotrebbe essere altrettanto buono doublema potrebbe essere più efficiente in termini di memoria e CPU, devi solo cambiarlo in un posto e non in dozzine.


+1 per il suggerimento typedef. Ti dà il meglio di entrambi i mondi: tipi e oggetti primitivi.
Karl Bielefeldt,

Cambiare il tipo di un membro della classe non è più difficile di un typedef; o intendevi rispetto ai primitivi?
MaHuJa,

2

Belle risposte già qui, ma c'è una cosa che vorrei aggiungere:

L'uso PixelCoordinatee la PixelSizeclasse rendono le cose molto specifiche. Un generale Coordinateo una Point2Dclasse potrebbe probabilmente soddisfare meglio le tue esigenze. A Point2Ddovrebbe essere una classe che implementa le operazioni punto / vettore più comuni. Potresti implementare una tale classe anche in modo generico ( Point2D<T>dove T può essere into doubleo qualcos'altro).

Le operazioni punto / vettore 2D sono così comuni per molte attività di programmazione della geometria 2D che per la mia esperienza tale decisione aumenterà notevolmente la possibilità di riutilizzare le operazioni 2D esistenti. Potrai evitare la necessità di ri-esecuzione di ciascuna PixelCoordinate, GPSCoordinatedi nuovo "whatsover" classe -Coordinate e ancora.


1
puoi anche farlo struct PixelCoordinate:Point2D<int>se vuoi una sicurezza di tipo adeguato
maniaco del cricchetto

0

Quindi voglio riformattare in modo che assomigli a questo:

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

Questo mi sembra significativamente più leggibile e sicuro rispetto al primo metodo.

È [più leggibile]. È anche una buona idea.

Ma ha senso creare classi PixelCoordinate e PixelSize?

Se rappresentano concetti diversi, ha senso (ovvero "sì").

O starei meglio usando solo std :: pair per ciascuno.

Potresti iniziare facendo typedef std::pair<int,int> PixelCoordinate, PixelSize;. In questo modo, copri la semantica (stai lavorando con coordinate e dimensioni dei pixel, non con std :: coppie nel tuo codice cliente) e se hai bisogno di estenderlo, devi semplicemente sostituire il typedef con una classe e il tuo codice cliente dovrebbe richiedono modifiche minime.

E ha senso avere una classe ZoomLevel, o dovrei semplicemente usarne una doppia?

Potresti anche digitarlo, ma userei un doublefinché non avrò bisogno di una funzionalità associata ad esso che non esiste per un doppio (ad esempio, avere un ZoomLevel::defaultvalore richiederebbe di definire una classe, ma fino a quando non hai qualcosa di simile quello, tienilo un doppio).

La mia intuizione dietro l'utilizzo delle classi per tutto si basa su questi presupposti [omesso per brevità]

Sono argomenti forti (dovresti sempre cercare di rendere le tue interfacce facili da usare correttamente e difficili da usare in modo errato).

Tuttavia, la maggior parte del codice C ++ che ho visto utilizza molti tipi primitivi e immagino che ci debba essere una buona ragione per questo.

Ci sono ragioni; in molti casi, tuttavia, tali motivi non sono validi. Un motivo valido per eseguire questa operazione potrebbe essere rappresentato dalle prestazioni, ma ciò significa che è necessario visualizzare questo tipo di codice come un'eccezione, non come una regola.

È una buona idea usare oggetti per qualcosa o ha degli aspetti negativi di cui non sono a conoscenza?

È generalmente una buona idea assicurarsi che se i tuoi valori appaiono sempre insieme (e non hanno alcun senso a parte) dovresti raggrupparli (per gli stessi motivi che hai citato nel tuo post).

Cioè, se hai coordinate che hanno sempre x, ye z, allora è molto meglio avere una coordinateclasse che trasmettere tre parametri ovunque.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.