Il termine "interfaccia" in C ++


11

Java fa una chiara distinzione tra classe interface. (Credo che anche C # lo faccia, ma non ne ho esperienza). Quando si scrive C ++, tuttavia, non esiste alcuna distinzione imposta dal linguaggio tra classe e interfaccia.

Di conseguenza, ho sempre visto l'interfaccia come una soluzione alternativa per la mancanza di ereditarietà multipla in Java. Fare una tale distinzione sembra arbitrario e insignificante in C ++.

Ho sempre avuto la tendenza a seguire l'approccio "scrivere le cose nel modo più ovvio", quindi se in C ++ ho quella che si potrebbe chiamare un'interfaccia in Java, ad esempio:

class Foo {
public:
  virtual void doStuff() = 0;
  ~Foo() = 0;
};

e ho quindi deciso che la maggior parte degli implementatori Foovoleva condividere alcune funzionalità comuni che avrei probabilmente scritto:

class Foo {
public:
  virtual void doStuff() = 0;
  ~Foo() {}
protected:
  // If it needs this to do its thing:
  int internalHelperThing(int);
  // Or if it doesn't need the this pointer:
  static int someOtherHelper(int);
};

Il che quindi non rende più questa un'interfaccia in senso Java.

Invece C ++ ha due concetti importanti, relativi allo stesso problema ereditario sottostante:

  1. virtual inhertiance
  2. Le classi senza variabili membro non possono occupare spazio aggiuntivo se utilizzate come base

    "Gli oggetti secondari della classe base possono avere dimensioni zero"

    Riferimento

Di quelli che cerco di evitare al primo posto dove possibile - è raro imbattersi in uno scenario in cui questo è veramente il design "più pulito". # 2 è tuttavia una differenza sottile, ma importante tra la mia comprensione del termine "interfaccia" e le caratteristiche del linguaggio C ++. Di conseguenza, attualmente non mi riferisco (quasi) mai a cose come "interfacce" in C ++ e parlo in termini di classi di base e dimensioni. Direi che nel contesto di C ++ "interfaccia" è un termine improprio.

Mi è venuto in mente che non molte persone fanno una tale distinzione.

  1. Posso perdere qualcosa consentendo (ad es. protected) Di non virtualfunzioni all'interno di una "interfaccia" in C ++? (La mia sensazione è esattamente l'opposto: una posizione più naturale per il codice condiviso)
  2. Il termine "interfaccia" è significativo in C ++ - implica solo puro virtualo sarebbe corretto chiamare classi C ++ senza variabili membro un'interfaccia ancora?

C # ha la stessa eredità multipla di interfacce, singola eredità di implementazione di Java, ma attraverso l'uso di metodi di estensione generici , internalHelperThingpuò essere simulato in quasi tutti i casi.
Sjoerd,

Si noti che è una cattiva pratica esporre membri virtuali.
Klaim,

un pubblico ~Foo() {}in una classe astratta è un errore in (quasi) ogni circostanza.
Lupo,

Risposte:


11

In C ++, il termine "interfaccia" non ha solo una definizione ampiamente accettata - quindi ogni volta che la userai, dovresti dire cosa intendi esattamente - una classe di base virtuale con o senza implementazioni predefinite, un file di intestazione, i membri pubblici di una classe arbitraria e così via.

Per quanto riguarda il tuo esempio: in Java (e simili in C #), il tuo codice probabilmente implicherebbe una separazione di preoccupazioni:

interface IFoo {/*  ... */} // here is your interface

class FooBase implements IFoo 
{
     // make default implementations for interface methods
}

class Foo extends FooBase
{
}

In C ++, puoi farlo ma non sei obbligato a farlo. E se ti piace chiamare una classe un'interfaccia se non ha variabili membro, ma contiene implementazioni predefinite per alcuni metodi, fallo e assicurati che tutti quelli con cui stai parlando sappiano cosa intendi.


3
Un altro possibile significato di "interfaccia", per un programmatore C ++, è le publicparti del .hfile.
David Thornley,

In che lingua è il tuo esempio di codice? Se questo è un tentativo di essere C ++ sto per piangere ...
Qix - MONICA È STATA MISTREATA il

@Qix: mantieniti facile, leggi di nuovo il mio post (indica chiaramente che il codice è in Java), e in caso tu abbia> 2000 punti, puoi modificare il mio post per aggiungere il codice di esempio C ++ equivalente, per rendere il mio esempio più chiaro.
Doc Brown,

Se quello è Java, allora è ancora sbagliato, e no, non posso modificarlo; non abbastanza personaggi da cambiare. Java non usa i due punti nell'implementazione / estensione, motivo per cui mi chiedevo se si trattasse di un tentativo di C ++ ...
Qix - MONICA È STATA MISTREATA il

@Qix: se trovi altri problemi sintattici, puoi tenerli come regalo di Natale :-)
Doc Brown il

7

Sembra un po 'che potresti essere caduto nella trappola di confondere il significato di ciò che un'interfaccia è concettualmente sia come un'implementazione (l'interfaccia - "i" minuscola), sia un'astrazione (un'interfaccia - "I" maiuscola ).

Per quanto riguarda i tuoi esempi, il tuo primo bit di codice è semplicemente una classe. Mentre la tua classe ha un'interfaccia nel senso che fornisce metodi per abilitare gli accessi al suo comportamento, non è un'interfaccia nel senso di una dichiarazione di interfaccia che fornisce uno strato di astrazione che rappresenta un tipo di comportamento che potresti desiderare che le classi strumento. La risposta di Doc Brown al tuo post ti mostra esattamente di cosa sto parlando qui.

Le interfacce sono spesso pubblicizzate come il "rimedio" per le lingue che non supportano l'ereditarietà multipla, ma ritengo che sia più un malinteso che una dura verità (e sospetto che potrei essere infiammato per averlo affermato!). Le interfacce non hanno davvero nulla a che fare con l'ereditarietà multipla in quanto non richiedono né l'ereditarietà né l'antenato per fornire compatibilità funzionale tra le classi o astrazione dell'implementazione per le classi. In effetti, possono permetterti di eliminare completamente l'eredità in modo efficace se desideri implementare il tuo codice in quel modo - non che io lo consiglierei del tutto, ma sto solo dicendo che potrestifallo. Quindi la realtà è che, indipendentemente dalle questioni di ereditarietà, le interfacce forniscono un mezzo con cui definire i tipi di classe, stabilendo le regole che determinano il modo in cui gli oggetti possono comunicare tra loro, quindi consentono di determinare il comportamento che le classi dovrebbero supportare senza dettare il metodo specifico utilizzato per implementare quel comportamento.

So di perdere qualcosa consentendo a funzioni (non protette) non virtuali di esistere all'interno di una "interfaccia" in C ++? (La mia sensazione è esattamente l'opposto: una posizione più naturale per il codice condiviso)

Un'interfaccia pura deve essere interamente astratta, perché consente alla definizione un contratto di compatibilità tra classi che potrebbe non aver ereditato il loro comportamento da un antenato comune. Quando implementato, si desidera scegliere se consentire l'estensione del comportamento dell'implementazione in una sottoclasse. Se i tuoi metodi non lo sono virtual, perdi la possibilità di estendere tale comportamento in un secondo momento se decidi di dover creare classi discendenti. Indipendentemente dal fatto che l'implementazione sia virtuale o meno, l'interfaccia definisce la compatibilità comportamentale in sé e per sé, mentre la classe fornisce l'implementazione di quel comportamento per le istanze che la classe rappresenta.

Il termine "interfaccia" è significativo in C ++ - implica solo puro virtuale o sarebbe corretto chiamare classi C ++ senza variabili membro un'interfaccia ancora?

Perdonami, ma è da molto tempo che non scrivo davvero un'applicazione seria in C ++. Ricordo che Interface è una parola chiave ai fini dell'astrazione come li ho descritti qui. Non chiamerei classi C ++ di alcun tipo un'interfaccia, direi invece che la classe ha un'interfaccia all'interno dei significati che ho descritto sopra. In tal senso, il termine È significativo, ma dipende in realtà dal contesto.


1
+1 per la distinzione tra "avere" e "essere" un'interfaccia.
Doc Brown,

6

Le interfacce Java non sono una "soluzione alternativa", sono una decisione deliberata di progettazione per evitare alcuni dei problemi con ereditarietà multipla come l'ereditarietà dei diamanti e per incoraggiare pratiche di progettazione che minimizzano l'accoppiamento.

La tua interfaccia con metodi protetti è un caso da manuale che deve "preferire la composizione all'eredità". Per citare Sutter e Alexandrescu nella sezione con quel nome dai loro eccellenti standard di codifica C ++ :

Evitare le tasse di successione: l'ereditarietà è la seconda relazione di accoppiamento più stretta in C ++, seconda solo all'amicizia. Un accoppiamento stretto è indesiderabile e dovrebbe essere evitato ove possibile. Pertanto, preferisci la composizione all'eredità, a meno che tu non sappia che quest'ultimo apporta davvero benefici al tuo design.

Includendo le funzioni di supporto nella tua interfaccia, potresti risparmiare un po 'di battitura ora, ma stai introducendo un accoppiamento che ti farà male lungo la strada. È quasi sempre meglio a lungo termine separare le funzioni di supporto e passare a Foo*.

L'STL ne è un ottimo esempio. Vengono estratte quante più funzioni di supporto possibili <algorithm>anziché essere nelle classi contenitore. Ad esempio, poiché sort()utilizza l'API del contenitore pubblico per svolgere il proprio lavoro, sai che puoi implementare il tuo algoritmo di ordinamento senza dover modificare alcun codice STL. Questo design consente a librerie come boost, che migliorano l'STL invece di sostituirlo, senza che l'STL abbia bisogno di sapere nulla di boost.


2

So di perdere qualcosa consentendo a funzioni (non protette) non virtuali di esistere all'interno di una "interfaccia" in C ++? (La mia sensazione è esattamente l'opposto: una posizione più naturale per il codice condiviso)

Lo penseresti, ma mettere un metodo protetto e non virtuale in una classe altrimenti astratta impone l'implementazione a chiunque scriva una sottoclasse. In questo modo si sconfigge lo scopo di un'interfaccia nel suo senso puro, che è quello di fornire un'impiallacciatura che nasconda ciò che è sotto.

Questo è uno di quei casi in cui non esiste una risposta unica e devi usare la tua esperienza e il tuo giudizio per prendere una decisione. Se puoi affermare con certezza al 100% che ogni possibile sottoclasse della tua classe altrimenti completamente virtuale Fooavrà sempre bisogno di un'implementazione del metodo protetto bar(), allora Fooè il posto giusto per questo. Una volta che hai una sottoclasse Bazche non ha bisogno bar(), o devi convivere con il fatto che Bazavrà accesso al codice che non dovrebbe o passare attraverso l'esercizio di riorganizzazione della gerarchia di classi. Il primo non è una buona pratica e il secondo può richiedere più tempo dei pochi minuti che ci sarebbero voluti per sistemare le cose in primo luogo.

Il termine "interfaccia" è significativo in C ++ - implica solo puro virtuale o sarebbe corretto chiamare classi C ++ senza variabili membro un'interfaccia ancora?

La sezione 10.4 dello standard C ++ fa un accenno all'utilizzo delle classi astratte per implementare le interfacce ma non le definisce formalmente. Il termine è significativo in un contesto informatico generale e chiunque sia competente dovrebbe capire che " Fooè un'interfaccia per (qualunque cosa)" implica una qualche forma di astrazione. Quelli con esposizione a linguaggi con costrutti di interfaccia definiti potrebbero pensare al puro virtuale, ma chiunque abbia effettivamente bisogno di lavorare Fooguarderà la sua definizione prima di procedere.


2
Qual è la differenza tra fornire una dichiarazione virtuale pura e una dichiarazione oltre all'implementazione? In entrambi i casi, ci deve essere un'implementazione e bazpuò sempre avere un'implementazione simile return false;o qualsiasi altra cosa. Se il metodo non si applica a tutte le sottoclassi, non appartiene alla classe astratta di base in nessuna forma.
David Thornley,

1
Penso che la tua ultima frase affermi che siamo sulla stessa pagina: non c'è motivo per cui non puoi avere metodi protetti in classi astratte, ma non dovrebbero essere più alti nella struttura ereditaria di quanto assolutamente necessario. Penso solo che se una classe deve falsificare un'implementazione, non è nel posto giusto nella struttura.
Blrfl,
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.