Non mi viene in mente alcun vantaggio (ma vedi la nota a JasonS in fondo), racchiudendo una riga di codice come funzione o subroutine. Tranne forse che puoi nominare la funzione qualcosa di "leggibile". Ma puoi anche commentare la linea. E dato che racchiudere una riga di codice in una funzione costa memoria di codice, spazio di stack e tempo di esecuzione, mi sembra che sia principalmente controproducente. In una situazione di insegnamento? Potrebbe avere un senso. Ma ciò dipende dalla classe degli studenti, dalla loro preparazione in anticipo, dal curriculum e dall'insegnante. Principalmente, penso che non sia una buona idea. Ma questa è la mia opinione.
Il che ci porta alla conclusione. La vostra vasta area di domande è stata, per decenni, oggetto di un dibattito e rimane ancora oggi un argomento di dibattito. Quindi, almeno mentre leggo la tua domanda, mi sembra una domanda basata sull'opinione (come l'hai fatta tu.)
Potrebbe essere allontanato dall'essere basato sull'opinione come è, se si dovesse essere più dettagliati sulla situazione e descrivere attentamente gli obiettivi che si ritenevano primari. Migliore è la definizione degli strumenti di misurazione, più obiettivi potrebbero essere le risposte.
In linea di massima, si desidera effettuare le seguenti operazioni per qualsiasi codice. (Per il seguito, suppongo che stiamo confrontando diversi approcci che raggiungono tutti gli obiettivi. Ovviamente, qualsiasi codice che non riesce a svolgere le attività necessarie è peggiore del codice che ha successo, indipendentemente da come è scritto.)
- Sii coerente con il tuo approccio, in modo che un'altra lettura del tuo codice possa sviluppare una comprensione di come approcci il tuo processo di codifica. Essere incoerenti è probabilmente il crimine peggiore possibile. Non solo rende difficile per gli altri, ma rende difficile per te tornare al codice anni dopo.
- Nella misura del possibile, prova a sistemare le cose in modo che l'inizializzazione di varie sezioni funzionali possa essere eseguita indipendentemente dall'ordinamento. Dove è richiesto l'ordinazione, se è dovuto a un accoppiamento stretto di due funzioni secondarie altamente correlate, prendere in considerazione una singola inizializzazione per entrambi in modo che possa essere riordinato senza causare danni. Se ciò non è possibile, documentare il requisito di ordinazione di inizializzazione.
- Incapsulare la conoscenza esattamente in un posto, se possibile. Le costanti non devono essere duplicate ovunque nel codice. Le equazioni che risolvono per alcune variabili dovrebbero esistere in un unico posto. E così via. Se ti ritrovi a copiare e incollare un insieme di linee che eseguono alcuni comportamenti necessari in una varietà di posizioni, considera un modo per acquisire tale conoscenza in un unico luogo e utilizzarla dove necessario. Ad esempio, se si dispone di una struttura ad albero che deve essere percorsa in un modo specifico, non farloreplicare il codice tree-walking in ogni punto in cui è necessario scorrere i nodi dell'albero. Invece, cattura il metodo del camminare sugli alberi in un posto e usalo. In questo modo, se l'albero cambia e il metodo di camminata cambia, hai solo un posto di cui preoccuparti e tutto il resto del codice "funziona bene".
- Se distribuisci tutte le tue routine su un enorme foglio di carta piatto, con le frecce che le collegano come vengono chiamate da altre routine, vedrai in ogni applicazione che ci saranno "gruppi" di routine che hanno un sacco di frecce tra loro ma solo poche frecce fuori dal gruppo. Quindi ci saranno confini naturali di routine strettamente accoppiate e connessioni vagamente accoppiate tra altri gruppi di routine strettamente accoppiate. Usa questo fatto per organizzare il tuo codice in moduli. Ciò limiterà sostanzialmente la complessità apparente del codice.
Quanto sopra è generalmente vero su tutta la codifica. Non ho discusso dell'uso di parametri, variabili globali locali o statiche, ecc. Il motivo è che per la programmazione integrata lo spazio dell'applicazione pone spesso nuovi vincoli estremi e molto significativi ed è impossibile discuterne tutti senza discutere di ogni applicazione incorporata. E questo non sta accadendo qui, comunque.
Questi vincoli possono essere uno (e più) di questi:
- Gravi limiti di costo che richiedono MCU estremamente primitive con RAM minuscola e quasi nessun conteggio dei pin I / O. Per questi, si applicano interi nuovi set di regole. Ad esempio, potrebbe essere necessario scrivere nel codice assembly perché non c'è molto spazio nel codice. Potrebbe essere necessario utilizzare SOLO variabili statiche perché l'uso di variabili locali è troppo costoso e richiede tempo. Potrebbe essere necessario evitare l'uso eccessivo di subroutine perché (ad esempio, alcune parti Microchip PIC) ci sono solo 4 registri hardware in cui memorizzare gli indirizzi di ritorno della subroutine. Quindi potresti dover "appiattire" in modo drammatico il tuo codice. Eccetera.
- Gravi limiti di potenza che richiedono un codice attentamente predisposto per l'avvio e l'arresto della maggior parte dell'MCU e la limitazione severa del tempo di esecuzione del codice quando si esegue a piena velocità. Ancora una volta, questo potrebbe richiedere alcuni codici di assemblaggio, a volte.
- Requisiti di temporizzazione severi. Ad esempio, ci sono volte in cui ho dovuto assicurarmi che la trasmissione di uno drain aperto 0 dovesse richiedere ESATTAMENTE lo stesso numero di cicli della trasmissione di un 1. E che anche il campionamento di questa stessa linea doveva essere eseguito con una fase relativa esatta a questo tempismo. Ciò significava che C NON poteva essere usato qui. L'UNICO modo possibile per garantire tale garanzia è creare con cura il codice di assemblaggio. (E anche allora, non sempre su tutti i design ALU.)
E così via. (Anche il codice di cablaggio per la strumentazione medica critica per la vita ha un suo intero mondo.)
Il risultato qui è che la codifica incorporata spesso non è gratuita, in cui è possibile programmare come su una workstation. Esistono spesso motivi severi e competitivi per un'ampia varietà di vincoli molto difficili. E questi possono fortemente discutere contro le risposte più tradizionali e di borsa .
Per quanto riguarda la leggibilità, trovo che il codice sia leggibile se è scritto in modo coerente che posso imparare mentre lo leggo. E dove non c'è un tentativo deliberato di offuscare il codice. Non c'è davvero molto di più richiesto.
Il codice leggibile può essere abbastanza efficiente e può soddisfare tutti i requisiti sopra menzionati. La cosa principale è che capisci esattamente cosa produce ogni riga di codice che scrivi a livello di assembly o macchina, mentre lo scrivi. Il C ++ pone un grave onere per il programmatore qui perché ci sono molte situazioni in cui frammenti identici di codice C ++ in realtà generano frammenti diversi di codice macchina con prestazioni notevolmente diverse. Ma C, generalmente, è principalmente una lingua "ciò che vedi è ciò che ottieni". Quindi è più sicuro al riguardo.
EDIT per JasonS:
Uso C dal 1978 e C ++ dal 1987 circa e ho avuto molta esperienza nell'uso di entrambi i mainframe, i minicomputer e (principalmente) le applicazioni integrate.
Jason fa apparire un commento sull'uso di 'inline' come modificatore. (Nella mia prospettiva, questa è una funzionalità relativamente "nuova" perché semplicemente non esisteva per forse metà della mia vita o più usando C e C ++.) L'uso di funzioni in linea può effettivamente effettuare tali chiamate (anche per una linea di codice) abbastanza pratico. Ed è molto meglio, ove possibile, che usare una macro a causa della digitazione che il compilatore può applicare.
Ma ci sono anche dei limiti. Il primo è che non puoi fare affidamento sul compilatore per "prendere il suggerimento". Potrebbe o no. E ci sono buoni motivi per non dare il suggerimento. (Per un esempio ovvio, se viene preso l'indirizzo della funzione, ciò richiede l'istanza della funzione e l'uso dell'indirizzo per effettuare la chiamata ... richiederà una chiamata. Il codice non può essere incorporato allora.) Ci sono anche altri motivi. I compilatori possono avere un'ampia varietà di criteri in base ai quali giudicano come gestire il suggerimento. E come programmatore, questo significa che devidedicare un po 'di tempo a conoscere quell'aspetto del compilatore oppure è probabile che tu prenda decisioni basate su idee imperfette. Quindi aggiunge un onere sia allo scrittore del codice che a qualsiasi lettore e anche a chiunque abbia intenzione di trasferire il codice su qualche altro compilatore.
Inoltre, i compilatori C e C ++ supportano la compilazione separata. Ciò significa che possono compilare un pezzo di codice C o C ++ senza compilare nessun altro codice correlato per il progetto. Per incorporare il codice, supponendo che il compilatore possa altrimenti scegliere di farlo, non solo deve avere la dichiarazione "nell'ambito" ma deve anche avere la definizione. Di solito, i programmatori lavoreranno per assicurarsi che ciò avvenga se utilizzano "inline". Ma è facile che si insinuino errori.
In generale, anche se uso in linea dove ritengo sia appropriato, tendo ad assumere che non posso fare affidamento su di esso. Se le prestazioni sono un requisito significativo e penso che l'OP abbia già scritto chiaramente che si è verificato un notevole calo delle prestazioni quando sono andati su un percorso più "funzionale", allora certamente sceglierei di evitare di fare affidamento su inline come pratica di codifica e seguirebbe invece un modello leggermente diverso, ma del tutto coerente di scrittura del codice.
Un'ultima nota su "inline" e le definizioni "in ambito" per una fase di compilazione separata. È possibile (non sempre affidabile) che il lavoro venga eseguito nella fase di collegamento. Ciò può accadere se e solo se un compilatore C / C ++ nasconde abbastanza dettagli nei file oggetto per consentire a un linker di agire su richieste "inline". Personalmente non ho sperimentato un sistema di linker (al di fuori di Microsoft) che supporti questa funzionalità. Ma può succedere. Ancora una volta, se si debba fare affidamento o meno dipenderà dalle circostanze. Ma di solito suppongo che questo non sia stato spinto sul linker, a meno che non lo sappia altrimenti sulla base di buone prove. E se faccio affidamento su di esso, sarà documentato in un posto di rilievo.
C ++
Per coloro che sono interessati, ecco un esempio del motivo per cui rimango abbastanza cauto nei confronti del C ++ quando codifico le applicazioni incorporate, nonostante la sua pronta disponibilità oggi. Lancerò alcuni termini che penso che tutti i programmatori C ++ incorporati debbano conoscere a freddo :
- specializzazione parziale del modello
- VTables
- oggetto base virtuale
- frame di attivazione
- cornice di attivazione svolgersi
- uso di puntatori intelligenti nei costruttori e perché
- ottimizzazione del valore di ritorno
Questo è solo un breve elenco. Se non sai già tutto di quei termini e del perché li ho elencati (e molti altri non li ho elencati qui) allora sconsiglierei l'uso del C ++ per il lavoro incorporato, a meno che non sia un'opzione per il progetto .
Diamo una rapida occhiata alla semantica delle eccezioni C ++ per avere solo un sapore.
UNB
UN
.
.
foo ();
String s;
foo ();
.
.
UN
B
Il compilatore C ++ vede la prima chiamata a foo () e può semplicemente consentire il verificarsi di un normale frame di attivazione, se foo () genera un'eccezione. In altre parole, il compilatore C ++ sa che a questo punto non è necessario alcun codice aggiuntivo per supportare il processo di svolgimento del frame coinvolto nella gestione delle eccezioni.
Ma una volta che String s è stato creato, il compilatore C ++ sa che deve essere correttamente distrutto prima che possa essere consentito lo svolgimento di un frame, se in seguito si verifica un'eccezione. Quindi la seconda chiamata a foo () è semanticamente diversa dalla prima. Se la seconda chiamata a foo () genera un'eccezione (cosa che può o non può fare), il compilatore deve aver inserito un codice progettato per gestire la distruzione di String s prima di lasciare che si verifichi il consueto svolgimento del frame. Questo è diverso dal codice richiesto per la prima chiamata a foo ().
(È possibile aggiungere ulteriori decorazioni in C ++ per aiutare a limitare questo problema. Ma il fatto è che i programmatori che usano C ++ devono semplicemente essere molto più consapevoli delle implicazioni di ogni riga di codice che scrivono.)
A differenza del malloc di C, il nuovo C ++ utilizza le eccezioni per segnalare quando non è in grado di eseguire l'allocazione della memoria non elaborata. Così sarà 'dynamic_cast'. (Vedi il 3 ° ed. Di Stroustrup, Il linguaggio di programmazione C ++, pagine 384 e 385 per le eccezioni standard in C ++.) I compilatori possono consentire di disabilitare questo comportamento. Ma in generale dovrete sostenere un certo overhead a causa di prologhi ed epiloghi di gestione delle eccezioni correttamente formati nel codice generato, anche quando le eccezioni in realtà non hanno luogo e anche quando la funzione in fase di compilazione non ha effettivamente blocchi di gestione delle eccezioni. (Stroustrup l'ha lamentato pubblicamente.)
Senza una specializzazione parziale dei template (non tutti i compilatori C ++ lo supportano), l'uso dei template può portare al disastro per la programmazione integrata. Senza di essa, la fioritura del codice rappresenta un grave rischio che potrebbe uccidere in un lampo un progetto incorporato con memoria ridotta.
Quando una funzione C ++ restituisce un oggetto, viene creato e distrutto un temporaneo compilatore senza nome. Alcuni compilatori C ++ possono fornire un codice efficiente se viene utilizzato un costruttore di oggetti nell'istruzione return, anziché un oggetto locale, riducendo le esigenze di costruzione e distruzione di un oggetto. Ma non tutti i compilatori lo fanno e molti programmatori C ++ non sono nemmeno a conoscenza di questa "ottimizzazione del valore di ritorno".
Fornire a un costruttore di oggetti un singolo tipo di parametro può consentire al compilatore C ++ di trovare un programmatore in un percorso di conversione tra due tipi in modi completamente inaspettati. Questo tipo di comportamento "intelligente" non fa parte di C.
Una clausola catch che specifica un tipo base "suddivide" un oggetto derivato generato, poiché l'oggetto generato viene copiato usando il "tipo statico" della clausola catch e non il "tipo dinamico" dell'oggetto. Una fonte non insolita di sofferenza delle eccezioni (quando ritieni di poterti permettere persino eccezioni nel tuo codice incorporato).
I compilatori C ++ possono generare automaticamente costruttori, distruttori, copiatori e operatori di assegnazione per te, con risultati indesiderati. Ci vuole tempo per guadagnare facilità con i dettagli di questo.
Il passaggio di matrici di oggetti derivati a una funzione che accetta matrici di oggetti di base, raramente genera avvisi del compilatore ma produce quasi sempre comportamenti errati.
Poiché il C ++ non invoca il distruttore di oggetti parzialmente costruiti quando si verifica un'eccezione nel costruttore di oggetti, la gestione delle eccezioni nei costruttori di solito impone "puntatori intelligenti" per garantire che i frammenti costruiti nel costruttore vengano correttamente distrutti se si verifica un'eccezione lì . (Vedi Stroustrup, pagina 367 e 368.) Questo è un problema comune nello scrivere buone classi in C ++, ma ovviamente evitato in C poiché C non ha la semantica di costruzione e distruzione incorporata. Scrivere codice adeguato per gestire la costruzione di oggetti secondari all'interno di un oggetto significa scrivere codice che deve affrontare questo problema semantico univoco in C ++; in altre parole "scrivere intorno" comportamenti semantici in C ++.
C ++ può copiare oggetti passati a parametri oggetto. Ad esempio, nei seguenti frammenti, la chiamata "rA (x);" può far sì che il compilatore C ++ invochi un costruttore per il parametro p, per poi chiamare il costruttore copia per trasferire l'oggetto x al parametro p, quindi un altro costruttore per l'oggetto di ritorno (un temporaneo senza nome) della funzione rA, che ovviamente è copiato dal parametro p. Peggio ancora, se la classe A ha i suoi oggetti che hanno bisogno di essere costruiti, questo può teletrasportarsi in modo disastroso. (Il programmatore AC eviterebbe la maggior parte di questa spazzatura, ottimizzando a mano poiché i programmatori C non hanno una sintassi così utile e devono esprimere tutti i dettagli uno alla volta.)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Infine, una breve nota per i programmatori C. longjmp () non ha un comportamento portatile in C ++. (Alcuni programmatori C usano questo come una sorta di meccanismo di "eccezione".) Alcuni compilatori C ++ tenteranno effettivamente di sistemare le cose per ripulire quando viene preso il longjmp, ma quel comportamento non è portabile in C ++. Se il compilatore ripulisce gli oggetti costruiti, non è portatile. Se il compilatore non li ripulisce, gli oggetti non vengono distrutti se il codice lascia l'ambito degli oggetti costruiti a causa del longjmp e il comportamento non è valido. (Se l'uso di longjmp in foo () non lascia un ambito, allora il comportamento potrebbe andare bene.) Questo non è troppo spesso usato dai programmatori C incorporati ma dovrebbero essere consapevoli di questi problemi prima di usarli.