Perché il C ++ STL è così fortemente basato su modelli? (e non su * interfacce *)


211

Voglio dire, a parte il suo nome obbligatorio (la libreria di modelli standard) ...

Inizialmente il C ++ intendeva presentare i concetti di OOP in C. Cioè: potevi dire cosa poteva o non poteva fare un'entità specifica (indipendentemente da come lo fa) in base alla sua classe e alla sua gerarchia di classi. Alcune composizioni di abilità sono più difficili da descrivere in questo modo a causa delle problematiche dell'eredità multipla e del fatto che il C ++ supporta il concetto di interfacce in un modo un po 'goffo (rispetto a Java, ecc.), Ma è lì (e potrebbe essere migliorata).

E poi sono entrati in gioco i modelli, insieme alla STL. L'STL sembrava prendere i classici concetti OOP e buttarli a fondo, usando invece i template.

Dovrebbe esserci una distinzione tra i casi in cui i modelli vengono utilizzati per generalizzare i tipi in cui i tipi stessi sono irrilevanti per il funzionamento del modello (contenitori, per esempio). Avere un vector<int>senso ha perfettamente senso.

Tuttavia, in molti altri casi (iteratori e algoritmi), si suppone che i tipi basati su modelli seguano un "concetto" (Iteratore di input, Iteratore in avanti, ecc ...) in cui i dettagli effettivi del concetto sono definiti interamente dall'implementazione del modello funzione / classe e non dalla classe del tipo utilizzato con il modello, che è un po 'anti-uso di OOP.

Ad esempio, puoi dire alla funzione:

void MyFunc(ForwardIterator<...> *I);

Aggiornamento: dato che non era chiaro nella domanda originale, ForwardIterator può essere modellato per consentire qualsiasi tipo di ForwardIterator. Il contrario è avere ForwardIterator come concetto.

si aspetta un Iteratore in avanti solo osservando la sua definizione, dove è necessario esaminare l'implementazione o la documentazione per:

template <typename Type> void MyFunc(Type *I);

Due affermazioni che posso fare a favore dell'uso dei modelli: il codice compilato può essere reso più efficiente, compilando su misura il modello per ciascun tipo utilizzato, anziché utilizzare vtables. E il fatto che i modelli possono essere utilizzati con tipi nativi.

Tuttavia, sto cercando un motivo più profondo per cui abbandonare la OOP classica a favore del modello per la STL? (Supponendo che tu abbia letto così lontano: P)


4
Potresti dare un'occhiata a stackoverflow.com/questions/31693/… . La risposta accettata è un'eccellente spiegazione di quali modelli ti offrono rispetto ai generici.
James McMahon,

6
@Jonas: Non ha senso. Il vincolo sulla cache costa cicli di clock, motivo per cui è importante. Alla fine della giornata, sono i cicli di clock, non la cache, che definiscono le prestazioni. Memoria e cache sono importanti solo nella misura in cui incidono sui cicli di clock spesi. Inoltre, l'esperimento può essere fatto facilmente. Confronta, per esempio, std :: for_Each chiamato con un argomento functor, con l'approccio OOP / vtable equivalente. La differenza nelle prestazioni è sbalorditiva . Ecco perché viene utilizzata la versione del modello.
jalf

7
e non vi è alcun motivo per cui il codice ridondante riempia l'icache. Se istanzio il vettore <char> e il vettore <int> nel mio programma, perché il codice del vettore <char> dovrebbe essere caricato in icache mentre sto elaborando il vettore <int>? In effetti, il codice per il vettore <int> viene tagliato perché non deve includere il codice per casting, vtables e indiretto.
jalf,

3
Alex Stepanov spiega perché l'eredità e l'uguaglianza non giocano bene insieme.
fredoverflow,

6
@BerndJendrissek: Uhm, vicino, ma non te stesso. Sì, più costi di codice in termini di larghezza di banda della memoria e utilizzo della cache, se mai effettivamente utilizzati . Ma non v'è alcun motivo particolare per aspettarsi una vector<int>e vector<char>per essere utilizzato allo stesso tempo. Si potrebbe, certo, ma è possibile utilizzare qualsiasi due pezzi di codice, allo stesso tempo. Ciò non ha nulla a che fare con i modelli, C ++ o STL. Non c'è nulla nella esemplificazione delle vector<int>quali richiede vector<char>codice da caricare o eseguito.
jalf

Risposte:


607

La risposta breve è "perché il C ++ è passato". Sì, alla fine degli anni '70, Stroustrup intendeva creare un C aggiornato con funzionalità OOP, ma è successo molto tempo fa. Quando la lingua fu standardizzata nel 1998, non era più una lingua OOP. Era un linguaggio multi-paradigma. Sicuramente aveva un po 'di supporto per il codice OOP, ma aveva anche un linguaggio di template completo turing sovrapposto, permetteva la metaprogrammazione in fase di compilazione e la gente aveva scoperto la programmazione generica. All'improvviso, OOP non sembrò poi così importante. Non quando possiamo scrivere codice più semplice, più conciso e più efficiente utilizzando le tecniche disponibili attraverso modelli e programmazione generica.

OOP non è il santo graal. È un'idea carina, ed è stato piuttosto un miglioramento rispetto ai linguaggi procedurali negli anni '70 quando è stato inventato. Ma onestamente non è tutto ciò che è rotto. In molti casi è goffo e dettagliato e non promuove realmente il codice riutilizzabile o la modularità.

Questo è il motivo per cui la comunità C ++ è oggi molto più interessata alla programmazione generica e perché tutti stanno finalmente iniziando a capire che anche la programmazione funzionale è abbastanza intelligente. OOP da solo non è proprio uno spettacolo.

Prova a tracciare un grafico delle dipendenze di un ipotetico STL "OOP-ified". Quante lezioni dovrebbero conoscersi a vicenda? Ci sarebbero molte dipendenze. Saresti in grado di includere solo l' vectorintestazione, senza anche ottenere iteratoro anche inserire iostream? L'STL lo rende facile. Un vettore conosce il tipo di iteratore che definisce, e questo è tutto. Gli algoritmi STL non sanno nulla . Non è nemmeno necessario includere un'intestazione iteratore, anche se tutti accettano iteratori come parametri. Qual è più modulare allora?

L'STL potrebbe non seguire le regole di OOP come lo definisce Java, ma non raggiunge gli obiettivi di OOP? Non raggiunge riusabilità, accoppiamento basso, modularità e incapsulamento?

E non raggiunge questi obiettivi meglio di una versione ified di OOP?

Per quanto riguarda il motivo per cui l'STL è stato adottato nella lingua, sono successe diverse cose che hanno portato all'STL.

Innanzitutto, i modelli sono stati aggiunti a C ++. Sono stati aggiunti per lo stesso motivo per cui i generici sono stati aggiunti a .NET. Sembrava una buona idea poter scrivere cose come "contenitori di un tipo T" senza buttare via la sicurezza del tipo. Ovviamente, l'implementazione su cui si basarono era molto più complessa e potente.

Quindi le persone hanno scoperto che il meccanismo del modello che avevano aggiunto era persino più potente del previsto. E qualcuno ha iniziato a sperimentare l'uso dei modelli per scrivere una libreria più generica. Uno ispirato alla programmazione funzionale e uno che utilizzava tutte le nuove funzionalità di C ++.

Lo presentò al comitato del linguaggio C ++, che impiegò un po 'di tempo ad abituarsi perché sembrava così strano e diverso, ma alla fine si rese conto che funzionava meglio dei tradizionali equivalenti OOP che avrebbero dovuto includere altrimenti . Quindi hanno apportato alcune modifiche e l'hanno adottato nella libreria standard.

Non è stata una scelta ideologica, non è stata una scelta politica di "vogliamo essere OOP o no", ma molto pragmatica. Hanno valutato la biblioteca e hanno visto che funzionava molto bene.

In ogni caso, entrambi i motivi menzionati per favorire la STL sono assolutamente essenziali.

La libreria standard C ++ deve essere efficiente. Se fosse meno efficiente, per esempio, del codice C arrotolato a mano equivalente, la gente non lo userebbe. Ciò ridurrebbe la produttività, aumenterebbe la probabilità di bug e nel complesso sarebbe una cattiva idea.

E la STL deve lavorare con i tipi primitivi, perché i tipi primitivi sono tutto ciò che hai in C, e sono una parte importante di entrambe le lingue. Se l'STL non funzionasse con array nativi, sarebbe inutile .

La tua domanda ha il forte presupposto che OOP sia "il migliore". Sono curioso di sapere perché. Chiedete perché "hanno abbandonato la OOP classica". Mi chiedo perché avrebbero dovuto restare bloccati. Quali vantaggi avrebbe avuto?


22
È un buon commento, ma vorrei evidenziare un dettaglio. STL non è un "prodotto" di C ++. In effetti, STL, come concetto, esisteva prima di C ++ e C ++ era semplicemente un linguaggio efficiente con (quasi) potenza per la programmazione generica, quindi STL è stato scritto in C ++.
Igor Krivokon,

17
Dal momento che i commenti continuano a sollevarlo, sì, sono consapevole che il nome STL è ambiguo. Ma non riesco a pensare a un nome migliore per "la parte della libreria standard C ++ che è modellata su STL". Il nome di fatto per quella parte della libreria standard è solo "STL", anche se è strettamente inaccurato. :) Fintanto che le persone non usano STL come nome per l' intera libreria standard (compresi IOStreams e le intestazioni C stdlib), sono felice. :)
jalf

5
@einpoklum E cosa guadagneresti esattamente da una classe base astratta? Prendi std::setcome esempio. Non eredita da una classe base astratta. In che modo ciò limita il tuo utilizzo di std::set? C'è qualcosa che non puoi fare con un std::setperché non eredita da una classe base astratta?
Fredoverflow

22
@einpoklum, per favore, dai un'occhiata alla lingua Smalltalk, che Alan Kay ha progettato per essere una lingua OOP quando ha inventato il termine OOP. Non aveva interfacce. OOP non riguarda interfacce o classi base astratte. Stai per dire che "Java, che non ha niente a che vedere con l'inventore del termine OOP, è più OOP che C ++, che non ha nulla a che vedere con l'inventore del termine OOP"? Quello che vuoi dire è "C ++ non è abbastanza simile a Java per i miei gusti". È giusto, ma non ha nulla a che fare con OOP.
jalf

8
@MasonWheeler se questa risposta fosse un po 'una palese assurdità non vedresti letteralmente centinaia di sviluppatori in tutto il mondo che votano +1 su questo con solo tre persone che fanno diversamente
panda-34

88

La risposta più diretta a ciò che penso tu stia chiedendo / lamentandoti è questa: il presupposto che il C ++ sia un linguaggio OOP è un falso presupposto.

C ++ è un linguaggio multi-paradigma. Può essere programmato utilizzando i principi OOP, può essere programmato in modo procedurale, può essere programmato genericamente (modelli) e con C ++ 11 (precedentemente noto come C ++ 0x) alcune cose possono anche essere programmate funzionalmente.

I progettisti di C ++ vedono questo come un vantaggio, quindi sostengono che vincolare C ++ ad agire come un linguaggio puramente OOP quando la programmazione generica risolve il problema meglio e, beh, più genericamente , sarebbe un passo indietro.


4
"e con C ++ 0x alcune cose possono anche essere programmate funzionalmente" - può essere programmata funzionalmente senza quelle caratteristiche, solo più verbalmente.
Jonas Kölker,

3
@Tyler In effetti se vincolassi C ++ a OOP puro, rimarrai con Objective-C.
Justicle,

@TylerMcHenry: Avendo appena chiesto questo , trovo che ho appena pronunciato la stessa risposta, come si! Solo un punto. Vorrei che aggiungessi il fatto che la libreria standard non può essere utilizzata per scrivere codice orientato agli oggetti.
einpoklum,

74

La mia comprensione è che Stroustrup originariamente preferiva un design di container "OOP-style", e in realtà non vedeva altro modo per farlo. Alexander Stepanov è il responsabile dell'STL e i suoi obiettivi non includevano "renderlo orientato agli oggetti" :

Questo è il punto fondamentale: gli algoritmi sono definiti su strutture algebriche. Mi ci sono voluti un altro paio d'anni per capire che bisogna estendere il concetto di struttura aggiungendo requisiti di complessità agli assiomi regolari. ... Credo che le teorie degli iteratori siano fondamentali per l'Informatica come le teorie degli anelli o degli spazi di Banach sono centrali per la Matematica. Ogni volta che guardavo un algoritmo provavo a trovare una struttura su cui è definito. Quindi quello che volevo fare era descrivere genericamente gli algoritmi. Questo è quello che mi piace fare. Posso passare un mese lavorando su un noto algoritmo cercando di trovare la sua rappresentazione generica. ...

STL, almeno per me, rappresenta l'unico modo in cui la programmazione è possibile. È, in effetti, abbastanza diverso dalla programmazione C ++ in quanto è stato presentato e viene ancora presentato nella maggior parte dei libri di testo. Ma, vedi, non stavo cercando di programmare in C ++, stavo cercando di trovare il modo giusto di gestire il software. ...

Ho avuto molte false partenze. Ad esempio, ho passato anni a cercare di trovare un qualche vantaggio per l'ereditarietà e i virtuali, prima di capire perché quel meccanismo era fondamentalmente imperfetto e non doveva essere usato. Sono molto felice che nessuno possa vedere tutti i passaggi intermedi - molti di loro erano molto sciocchi.

(Spiega perché l'ereditarietà e i virtuali - ovvero il design orientato agli oggetti "erano fondamentalmente imperfetti e non dovrebbero essere utilizzati" nel resto dell'intervista).

Una volta Stepanov ha presentato la sua biblioteca a Stroustrup, Stroustrup e altri hanno attraversato sforzi erculei per renderlo conforme allo standard ISO C ++ (stessa intervista):

Il supporto di Bjarne Stroustrup è stato fondamentale. Bjarne voleva davvero STL nello standard e se Bjarne vuole qualcosa, lo ottiene. ... Mi ha persino costretto a fare cambiamenti in STL che non avrei mai fatto per nessun altro ... è la persona più single che conosco. Fa le cose. Gli ci volle un po 'di tempo per capire di cosa trattava STL, ma quando lo fece, fu pronto a farcela. Ha anche contribuito a STL sostenendo l'opinione che più di un modo di programmazione era valido - contro la fine di flak e hype per più di un decennio, e perseguendo una combinazione di flessibilità, efficienza, sovraccarico e sicurezza del tipo in modelli che hanno reso possibile la STL. Vorrei affermare chiaramente che Bjarne è il principale designer di lingue della mia generazione.


2
Intervista interessante. Abbastanza sicuro di averlo letto qualche tempo fa, ma valeva sicuramente la pena ripeterlo. :)
jalf

3
Una delle interviste più interessanti sulla programmazione che abbia mai letto. Anche se mi lascia sete di maggiori dettagli ...
Felixyz,

Molte delle lamentele che fa su linguaggi come Java ("Non puoi scrivere un max () generico in Java che accetta due argomenti di qualche tipo e ha un valore di ritorno dello stesso tipo") erano rilevanti solo per le versioni molto antiche della lingua, prima che fossero aggiunti i generici. Già dall'inizio era noto che alla fine sarebbero stati aggiunti i generici (una volta individuata una sintassi / semantica fattibile), quindi le sue critiche sono in gran parte prive di fondamento. Sì, i generici in qualche modo sono necessari per preservare la sicurezza dei tipi in un linguaggio tipicamente statico, ma no, che non rende OO inutile.
Qualcuno il

1
@SomeGuy Non sono lamentele su Java di per sé. Sta parlando della "programmazione" OO "standard" di SmallTalk o, diciamo, di Java ". L'intervista è della fine degli anni '90 (menziona il lavoro in SGI, che ha lasciato nel 2000 per lavorare in AT&T). I generici sono stati aggiunti a Java solo nel 2004 nella versione 1.5 e sono una deviazione dal modello OO "standard".
melpomene,

24

La risposta si trova in questa intervista con Stepanov, l'autore della STL:

Sì. STL non è orientato agli oggetti. Penso che l'orientamento agli oggetti sia quasi una bufala come l'intelligenza artificiale. Devo ancora vedere un pezzo interessante di codice che proviene da queste persone OO.


Bel gioiello; Sai da che anno è?
Kos,

2
@Kos, secondo web.archive.org/web/20000607205939/http://www.stlport.org/… la prima versione della pagina collegata è del 7 giugno 2001. La pagina stessa in fondo dice Copyright 2001- del 2008.
alfC

@Kos Stepanov menziona il lavoro in SGI nella prima risposta. Ha lasciato SGI nel maggio 2000, quindi presumibilmente l'intervista è più vecchia di così.
melpomene,

18

Perché un design OOP puro per una libreria di strutture dati e algoritmi sarebbe migliore ?! OOP non è la soluzione per ogni cosa.

IMHO, STL è la libreria più elegante che abbia mai visto :)

per la tua domanda,

non è necessario il polimorfismo di runtime, è effettivamente un vantaggio per STL implementare la libreria usando il polimorfismo statico, ciò significa efficienza. Prova a scrivere un ordinamento o una distanza generici o qualsiasi altro algoritmo che si applichi a TUTTI i contenitori! il tuo ordinamento in Java chiamerebbe funzioni dinamiche attraverso n-livelli da eseguire!

Hai bisogno di cose stupide come Boxing e Unboxing per nascondere cattivi presupposti dei cosiddetti linguaggi Pure OOP.

L'unico problema che vedo con STL e i modelli in generale sono i terribili messaggi di errore. Che sarà risolto usando Concetti in C ++ 0X.

Confrontare STL con le raccolte in Java è come confrontare Taj Mahal a casa mia :)


12
Cosa, Taj Mahal è piccolo ed elegante, e la tua casa ha le dimensioni di una montagna e un casino completo? ;)
jalf

I concetti non fanno più parte di c ++ 0x. Alcuni dei messaggi di errore possono essere prevenuti usando static_assertforse.
KitsuneYMG,

GCC 4.6 ha migliorato i messaggi di errore del modello e credo che 4.7+ siano ancora migliori con esso.
David Stone,

Un concetto è essenzialmente l '"interfaccia" richiesta dall'OP. L'unica differenza è che "l'ereditarietà" da un concetto è implicita (se una classe ha tutte le giuste funzioni membro, è automaticamente un sottotipo del concetto) piuttosto che esplicita (una classe Java deve dichiarare esplicitamente che implementa un'interfaccia) . Tuttavia, sia il sottotitolo implicito che quello esplicito sono OO validi e alcuni linguaggi OO hanno ereditarietà implicita che funziona esattamente come Concetti. Quindi quello che viene detto qui è fondamentalmente "OO fa schifo: usa i modelli. Ma i modelli hanno problemi, quindi usa i Concetti (che sono OO)."
Qualcuno il

11

Si suppone che i tipi basati su modelli seguano un "concetto" (Iteratore di input, Iteratore in avanti, ecc ...) in cui i dettagli effettivi del concetto sono definiti interamente dall'implementazione della funzione / classe del modello e non dalla classe del tipo usato con il modello, che è un po 'anti-uso di OOP.

Penso che tu fraintenda l'uso previsto dei concetti con i modelli. Forward Iterator, ad esempio, è un concetto molto ben definito. Per trovare le espressioni che devono essere valide affinché una classe possa essere un Iteratore in avanti e la loro semantica inclusa la complessità computazionale, guardate lo standard o http://www.sgi.com/tech/stl/ForwardIterator.html (devi seguire i collegamenti a Input, Output e Trivial Iterator per vedere tutto).

Quel documento è un'interfaccia perfettamente valida e "i dettagli reali del concetto" sono definiti proprio lì. Non sono definiti dalle implementazioni di Forward Iterator, né sono definiti dagli algoritmi che utilizzano Forward Iterator.

Le differenze nel modo in cui vengono gestite le interfacce tra STL e Java sono tre volte:

1) STL definisce espressioni valide usando l'oggetto, mentre Java definisce metodi che devono essere richiamabili sull'oggetto. Naturalmente un'espressione valida potrebbe essere una chiamata di metodo (funzione membro), ma non deve esserlo.

2) Le interfacce Java sono oggetti di runtime, mentre i concetti STL non sono visibili in fase di runtime anche con RTTI.

3) Se non si riescono a rendere valide le espressioni valide richieste per un concetto STL, si ottiene un errore di compilazione non specificato quando si crea un'istanza di un modello con il tipo. Se non si implementa un metodo richiesto di un'interfaccia Java, viene visualizzato un errore di compilazione specifico che lo dice.

Questa terza parte è se ti piace una sorta di "tipizzazione" (in fase di compilazione): le interfacce possono essere implicite. In Java, le interfacce sono in qualche modo esplicite: una classe "è" Iterabile se e solo se dice che implementa Iterable. Il compilatore può verificare che le firme dei suoi metodi siano tutte presenti e corrette, ma la semantica è ancora implicita (cioè sono documentate o meno, ma solo più codici (unit test) possono dirti se l'implementazione è corretta).

In C ++, come in Python, sia la semantica che la sintassi sono implicite, sebbene in C ++ (e in Python se si ottiene il preprocessore di tipo forte) si ottiene un aiuto dal compilatore. Se un programmatore richiede una dichiarazione esplicita di interfacce simile a Java da parte della classe di implementazione, l'approccio standard consiste nell'utilizzare tratti di tipo (e l'ereditarietà multipla può impedire che ciò sia troppo dettagliato). Ciò che manca, rispetto a Java, è un singolo modello che posso istanziare con il mio tipo e che compilerà se e solo se tutte le espressioni richieste sono valide per il mio tipo. Questo mi direbbe se ho implementato tutti i bit richiesti, "prima di usarlo". Questa è una comodità, ma non è il nucleo di OOP (e continua a non testare la semantica,

STL può essere o meno sufficientemente OO per i tuoi gusti, ma certamente separa l'interfaccia in modo chiaro dall'implementazione. Non ha la capacità di Java di riflettere sulle interfacce e segnala diversamente le violazioni dei requisiti di interfaccia.

puoi dire la funzione ... si aspetta un Iteratore in avanti solo guardando la sua definizione, dove avresti bisogno di guardare l'implementazione o la documentazione per ...

Personalmente penso che i tipi impliciti siano un punto di forza, se usati in modo appropriato. L'algoritmo dice cosa fa con i suoi parametri template, e l'implementatore si assicura che quelle cose funzionino: è esattamente il comune denominatore di cosa dovrebbero fare le "interfacce". Inoltre, con STL, è improbabile che tu stia usando, diciamo, in std::copybase alla ricerca della sua dichiarazione in avanti in un file di intestazione. I programmatori dovrebbero capire cosa prende una funzione in base alla sua documentazione, non solo alla firma della funzione. Questo è vero in C ++, Python o Java. Ci sono limitazioni su ciò che può essere ottenuto digitando in qualsiasi lingua e provare a usare la digitazione per fare qualcosa che non fa (controllare la semantica) sarebbe un errore.

Detto questo, gli algoritmi STL di solito denominano i parametri del modello in modo da chiarire quale concetto è richiesto. Tuttavia, ciò serve a fornire utili informazioni supplementari nella prima riga della documentazione, non a rendere le dichiarazioni più informative. Ci sono più cose che devi sapere di quelle che possono essere incapsulate nei tipi di parametri, quindi devi leggere i documenti. (Ad esempio negli algoritmi che accettano un intervallo di input e un iteratore di output, è probabile che l'iteratore di output abbia bisogno di "spazio" sufficiente per un certo numero di output in base alla dimensione dell'intervallo di input e forse ai valori ivi contenuti. Prova a scriverlo con forza. )

Ecco Bjarne su interfacce dichiarate esplicitamente: http://www.artima.com/cppsource/cpp0xP.html

In generici, un argomento deve appartenere a una classe derivata da un'interfaccia (l'equivalente C ++ all'interfaccia è una classe astratta) specificato nella definizione del generico. Ciò significa che tutti i tipi di argomenti generici devono rientrare in una gerarchia. Ciò impone vincoli non necessari ai progetti richiede una previsione irragionevole da parte degli sviluppatori. Ad esempio, se scrivi un generico e definisco una classe, le persone non possono usare la mia classe come argomento per il tuo generico a meno che non conosca l'interfaccia che hai specificato e da cui ne abbia derivato la classe. È rigido.

Guardandolo al contrario, con la digitazione di anatra è possibile implementare un'interfaccia senza sapere che l'interfaccia esiste. Oppure qualcuno può scrivere un'interfaccia deliberatamente in modo tale che la tua classe la implementi, dopo aver consultato i tuoi documenti per vedere che non chiedono nulla che non fai già. Flessibile.


Sulle interfacce dichiarate esplicitamente, due parole: digitare le classi. (Che sono già ciò che Stepanov intende per "concetto".)
pione

"Se non riesci a rendere valide le espressioni valide richieste per un concetto STL, ricevi un errore di compilazione non specificato quando crei un'istanza di un modello con il tipo." - è falso. Passare qualcosa alla stdbiblioteca che non corrisponde a un concetto è di solito "mal formato, non è necessaria alcuna diagnosi".
Yakk - Adam Nevraumont,

È vero, stavo giocando veloce e sciolto con il termine "valido". Volevo solo dire se il compilatore non è in grado di compilare una delle espressioni richieste, quindi segnalerà qualcosa.
Steve Jessop,

8

"OOP per me significa solo messaggistica, conservazione locale, protezione e occultamento del processo statale, ed estremo legame tardivo di tutte le cose. Può essere fatto in Smalltalk e in LISP. Esistono probabilmente altri sistemi in cui ciò è possibile, ma Non ne sono consapevole. " - Alan Kay, creatore di Smalltalk.

C ++, Java e la maggior parte delle altre lingue sono tutte abbastanza lontane dalla classica OOP. Detto questo, argomentare per ideologie non è terribilmente produttivo. Il C ++ non è puro in alcun senso, quindi implementa funzionalità che sembra avere senso pragmatico al momento.


7

STL è iniziato con l'intenzione di fornire una vasta libreria che copra l'algoritmo più comunemente usato - con l'obiettivo di comportamento e prestazioni coerenti . Il modello è stato considerato un fattore chiave per rendere fattibile l'implementazione e l'obiettivo.

Giusto per fornire un altro riferimento:

Al Stevens Interviste Alex Stepanov, nel marzo 1995 di DDJ:

Stepanov ha spiegato la sua esperienza lavorativa e la scelta fatta verso una vasta libreria di algoritmi, che alla fine si è evoluta in STL.

Raccontaci qualcosa del tuo interesse a lungo termine nella programmazione generica

..... Poi mi è stato offerto un lavoro presso i Bell Laboratories che lavorano nel gruppo C ++ sulle librerie C ++. Mi hanno chiesto se potevo farlo in C ++. Certo, non conoscevo C ++ e, naturalmente, ho detto che potevo. Ma non potevo farlo in C ++, perché nel 1987 il C ++ non aveva modelli, che sono essenziali per abilitare questo stile di programmazione. L'ereditarietà era l'unico meccanismo per ottenere la genericità e non era sufficiente.

Anche ora l'ereditarietà C ++ non è molto utile per la programmazione generica. Discutiamo del perché. Molte persone hanno tentato di utilizzare l'ereditarietà per implementare strutture di dati e classi di container. Come sappiamo ora, ci sono stati pochi tentativi riusciti. L'ereditarietà C ++ e lo stile di programmazione ad esso associato sono notevolmente limitati. È impossibile implementare un design che includa una cosa banale come l'uguaglianza che lo utilizza. Se inizi con una classe base X alla radice della tua gerarchia e definisci un operatore di uguaglianza virtuale su questa classe che accetta un argomento di tipo X, quindi ricava la classe Y dalla classe X. Qual è l'interfaccia dell'uguaglianza? Ha l'uguaglianza che confronta Y con X. Usando gli animali come esempio (le persone OO amano gli animali), definiscono il mammifero e derivano la giraffa dal mammifero. Quindi definire un compagno di funzione membro, dove l'animale si accoppia con l'animale e restituisce un animale. Quindi si ricava la giraffa dall'animale e, naturalmente, ha una funzione di accoppiamento in cui la giraffa si accoppia con l'animale e restituisce un animale. Non è sicuramente quello che vuoi. Mentre l'accoppiamento potrebbe non essere molto importante per i programmatori C ++, l'uguaglianza lo è. Non conosco un singolo algoritmo in cui l'uguaglianza di qualche tipo non viene utilizzata.


5

Il problema di base con

void MyFunc(ForwardIterator *I);

come si ottiene in sicurezza il tipo di cosa restituito dall'iteratore? Con i modelli, questo viene fatto per te al momento della compilazione.


1
Bene, neanche io: 1. Non provo a ottenerlo, poiché sto scrivendo un codice generico. Oppure, ottenerlo utilizzando qualsiasi meccanismo di riflessione offerto da C ++ in questi giorni.
einpoklum,

2

Per un momento, pensiamo alla libreria standard come sostanzialmente un database di raccolte e algoritmi.

Se hai studiato la storia dei database, sicuramente sai che all'inizio i database erano per lo più "gerarchici". I database gerarchici corrispondevano molto da vicino al classico OOP - in particolare, la varietà a eredità singola, come quella usata da Smalltalk.

Nel tempo, è diventato evidente che i database gerarchici potevano essere utilizzati per modellare quasi tutto, ma in alcuni casi il modello a eredità singola era abbastanza limitante. Se avevi una porta di legno, era utile poterla guardare sia come una porta, sia come un pezzo di materia prima (acciaio, legno, ecc.)

Quindi, hanno inventato i database dei modelli di rete. I database dei modelli di rete corrispondono molto da vicino all'eredità multipla. C ++ supporta l'ereditarietà multipla completamente, mentre Java supporta una forma limitata (puoi ereditare da una sola classe, ma puoi anche implementare tutte le interfacce che desideri).

I database dei modelli gerarchici e dei modelli di rete sono in gran parte svaniti dall'uso generale (anche se alcuni rimangono in nicchie abbastanza specifiche). Per la maggior parte degli scopi, sono stati sostituiti da database relazionali.

Gran parte del motivo per cui i database relazionali sono subentrati è la versatilità. Il modello relazionale è funzionalmente un superset del modello di rete (che a sua volta è un superset del modello gerarchico).

Il C ++ ha ampiamente seguito lo stesso percorso. La corrispondenza tra eredità singola e modello gerarchico e tra eredità multipla e modello di rete è abbastanza ovvia. La corrispondenza tra i modelli C ++ e il modello gerarchico potrebbe essere meno ovvia, ma è comunque abbastanza vicina.

Non ne ho visto una prova formale, ma credo che le capacità dei template siano un superset di quelle fornite dall'ereditarietà multipla (che è chiaramente un superset di single inerhitance). L'unica parte delicata è che i modelli sono per lo più associati staticamente, ovvero tutta l'associazione avviene in fase di compilazione, non in fase di esecuzione. Pertanto, una prova formale che l'eredità fornisce un superset delle capacità dell'eredità può essere in qualche modo difficile e complessa (o può anche essere impossibile).

In ogni caso, penso che sia la maggior parte della vera ragione per cui C ++ non usa l'ereditarietà per i suoi contenitori - non c'è motivo reale per farlo, perché l'ereditarietà fornisce solo un sottoinsieme delle funzionalità fornite dai modelli. Poiché i modelli sono sostanzialmente una necessità in alcuni casi, potrebbero anche essere usati quasi ovunque.


0

Come si fanno i confronti con ForwardIterator *? Cioè, come controlli se l'articolo che hai è quello che stai cercando o lo hai superato?

Il più delle volte, userei qualcosa del genere:

void MyFunc(ForwardIterator<MyType>& i)

il che significa che so che sto puntando a MyType e so come confrontarli. Anche se sembra un modello, in realtà non è (nessuna parola chiave "modello").


puoi semplicemente usare gli operatori <,> e = del tipo e non sapere cosa siano (anche se questo potrebbe non essere quello che volevi dire)
lhahne,

A seconda del contesto, quelli potrebbero non avere alcun senso o potrebbero funzionare bene. Difficile dirlo senza sapere di più su MyType, cosa che, presumibilmente, l'utente fa e noi no.
Tanktalus,

0

Questa domanda ha molte grandi risposte. Va anche detto che i modelli supportano un design aperto. Con lo stato attuale dei linguaggi di programmazione orientati agli oggetti, è necessario utilizzare il modello di visitatore quando si affrontano tali problemi e il vero OOP dovrebbe supportare l'associazione dinamica multipla. Vedi Open Multi-Methods per C ++, P. Pirkelbauer, et.al.per una lettura molto interessante.

Un altro punto interessante dei modelli è che possono essere utilizzati anche per il polimorfismo di runtime. Per esempio

template<class Value,class T>
Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func)
    {
    auto dt=(t_end-t_0)/N;
    for(size_t k=0;k<N;++k)
        {y_0+=func(t_0 + k*dt,y_0)*dt;}
    return y_0;
    }

Nota che questa funzione funzionerà anche se Valueè un vettore di qualche tipo ( non std :: vector, che dovrebbe essere chiamatostd::dynamic_array per evitare confusione)

Se func è piccola, questa funzione guadagnerà molto dall'allineamento. Esempio di utilizzo

auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y)
    {return y;});

In questo caso, dovresti conoscere la risposta esatta (2.718 ...), ma è facile costruire un semplice ODE senza soluzione elementare (Suggerimento: usa un polinomio in y).

Ora, hai una grande espressione in func, e usi il risolutore ODE in molti posti, quindi il tuo eseguibile viene inquinato da istanze di template ovunque. Cosa fare? La prima cosa da notare è che funziona un normale puntatore a funzione. Quindi si desidera aggiungere il curry in modo da scrivere un'interfaccia e un'istanza esplicita

class OdeFunction
    {
    public:
        virtual double operator()(double t,double y) const=0;
    };

template
double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func);

Ma l'istanza di cui sopra funziona solo per double, perché non scrivere l'interfaccia come modello:

template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

e specializzati per alcuni tipi di valore comuni:

template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func);

template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components)

template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components)

template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above)

Se la funzione fosse stata progettata prima attorno a un'interfaccia, allora saresti stato costretto a ereditare da quell'ABC. Ora hai questa opzione, oltre al puntatore a funzione, lambda o qualsiasi altro oggetto funzione. La chiave qui è che dobbiamo avere operator()()e dobbiamo essere in grado di usare alcuni operatori aritmetici sul suo tipo di ritorno. Pertanto, in questo caso la macchina modello si spezzerebbe se C ++ non avesse un sovraccarico dell'operatore.


-1

Il concetto di separare l'interfaccia dall'interfaccia e di essere in grado di scambiare le implementazioni non è intrinseco alla programmazione orientata agli oggetti. Credo sia un'idea nata nello sviluppo basato su componenti come Microsoft COM. (Vedi la mia risposta su Che cos'è lo sviluppo guidato da componenti?) Crescendo e imparando il C ++, le persone sono state esaltate dall'ereditarietà e dal polimorfismo. Non è stato fino agli anni '90 che la gente ha iniziato a dire "Programma a una 'interfaccia', non a una 'implementazione'" e "Favorisci 'la composizione di oggetti' rispetto a" eredità di classe "." (entrambi citati da GoF tra l'altro).

Quindi Java arrivò con il Garbage Collector integrato e la interfaceparola chiave, e all'improvviso divenne pratico separare effettivamente l'interfaccia e l'implementazione. Prima che tu lo sapessi, l'idea è diventata parte dell'OO. C ++, modelli e STL precedono tutto questo.


Concordato che le interfacce non sono solo OO. Ma il polimorfismo delle abilità nel sistema dei tipi è (era in Simula negli anni '60). Le interfacce dei moduli esistevano in Modula-2 e Ada, ma secondo me funzionavano nel sistema di tipi in modo diverso.
Andygavin,
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.