Progettare intorno a una costanza poco profonda con l'ereditarietà

5

Sfondo

Sto scrivendo una classe di gestione delle immagini. Per questa domanda sono interessanti due requisiti della classe:

  • Deve avere una "const" correttezza.
  • Deve consentire che gli alias delle immagini secondarie, le sezioni a.k.a. o l'affettatura, senza eseguire copie approfondite dei dati di immagine.

Per profonda costanza intendo il seguente. Considera questa classe di esempio:

class Image{
public:
    ...
    Image(const Image&);

    Image alias(int x0, int y0, int x1, int y1) const;
private:
    Buffer image_data;
};

Supponiamo che tu abbia una funzione che ha passato un riferimento const a un'immagine, in questo modo:

void foo(const Image& im){
    Image alias = im.alias(0,0, 10, 10);
    // Hold on, the mutable alias can modify the data in the const argument!
}

La chiamata alla funzione alias sopra sconfigge la costanza dell'argomento perché il contenuto dell'immagine dell'argomento può essere modificato tramite l'alias non const. Non posso restituire un const Image da alias perché in realtà non ha senso e il costruttore di copie di Image costruirà comunque un'istanza mutabile.

La mia soluzione (e l'ho vista spesso usata così non rivendico l'originalità qui) è di introdurre due classi (le interfacce in realtà, ma per la semplicità sono classi in questo esempio e il ritorno per valore è ok, in reality alias(...) restituisce un shared_ptr all'interfaccia):

class ConstImage{
public:
    ConstImage alias(...) const;
};

class Image{
public:
    Image alias(...);
};

Ora il seguente codice:

void foo(const Image& im){ 
    // compile error, cannot convert ConstImage to Image, perfect!
    Image alias = im.alias(...); 
}

E ovviamente cambieresti la firma in void foo(const ConstImage& im) in modo che tu possa accettare invocazioni come foo(const_im.alias(...)) o altri oggetti ConstImage .

Ora ovviamente vogliamo che foo(const ConstImage& im) sia callable (1) con un% mutabile% co_de e qui è dove arriva la domanda.

Domanda

Utilizzo dell'ereditarietà: Image renderà tutti i tipi class Image : public ConstImage utilizzabili tramite Image riferimenti e puntatori. Faciliterà anche il riutilizzo del codice tra le implementazioni const e non const che per necessità sono molto simili.

Questo risolve il problema sopra menzionato (1). Tuttavia, se si considera la relazione di ereditarietà come un'istruzione "è una", allora "un'immagine mutevole è un'immagine immutabile" non ha senso. Che parla contro l'utilizzo dell'eredità qui.

La mia domanda è, questo tipo di (ab) uso dell'eredità è ampiamente accettabile? C'è qualche altra soluzione migliore che ho trascurato? Si noti che la classe ConstImage è più un'interfaccia, quindi non è possibile la conversione del valore naive da Image a Image . Anche se uno potrebbe probabilmente inventarsi qualcosa di pazzo per consentirlo.

    
posta Emily L. 01.10.2015 - 16:21
fonte

8 risposte

2

Is there some other better solution that I have overlooked

Un Image "è un" ConstImage nel senso che può sempre essere "trattato come const". L'utilizzo dell'eredità strettamente per modellare le relazioni "IS A" è anche noto come "principio di sostituzione di Liskov" (LSP), che riguarda il comportamento. L'LSP è soddisfatto quando una classe ereditata può sempre sostituire la sua classe base senza violare la correttezza del programma. E fintanto che non chiami un metodo non const di un Image (che tipicamente non farai quando lo passi attraverso un riferimento ConstImage ), un Image si comporterà sempre come un ConstImage .

    
risposta data 01.10.2015 - 16:31
fonte
2

Crea una classe astratta ReadableImage . Deriva sia MutableImage che ImmutableImage (e anche ImageSlice ) da questa classe. Allora l'is-a tiene una relazione. Non prendere i parametri di ImmutableImage nelle funzioni a meno che la funzione non si aspetti realmente che l'immagine sia immutabile, ad esempio perché si aspetta che non venga modificata da un altro thread.

La parte fastidiosa di questo è l'affettamento, ma questo è un problema che si incontra sempre con le gerarchie di classi che rappresentano i dati in C ++.

    
risposta data 01.10.2015 - 23:31
fonte
2

Se accetti che Const significa semplicemente "non verrà manipolato da qui", come sembra implicare la maggior parte della tua domanda, una const_image non è immutabile.

Sembra che la tua classe sia un puntatore intelligente con accessori aggiuntivi e manipolatori (se non costanti).

Potresti trarne vantaggio basandoti su std::shared_ptr .

Successivamente, per emulare deep-const, la tua Image -class deve fare quanto segue:

  1. Assicurati che a un'istanza const manchino i manipolatori.

    È facile, assicurati che nessun metodo di manipolazione sia const -qualificato e che tutti i metodi di ispezione siano.

    int getX() const; // inspector
    int setX();       // manipulator
    
  2. Assicurati che un'istanza const non esponga alcun stato interno non-deep-const.

    Qualsiasi metodo di ispezione restituisce solo riferimenti con costanza o puntatori equivalenti e solo per lo stato che emula esso stesso deep-const o non ha nessuno stato collegato.
    Naturalmente le copie approfondite possono sempre essere restituite. (Le copie approfondite e le copie poco profonde sono banalmente equivalenti per lo stato senza collegamenti interni.)

  3. Assicurati che un'istanza non const non possa essere costruita da un'istanza const.

    Ora questo ultimo punto sembra un back-breaker, dato che C ++ non consente in modo sfortunato di creare un costruttore const .
    Fortunatamente, possiamo aggirarlo con più codice: basta dichiarare una seconda classe.

struct image;
struct const_image {
    // ctors, dtor, const inspectors
    // no manipulators, and only expose as const_image, other deep-const type or deep-copy
private:
    friend class image;
    // explicitly defaulted (move-)assignment-operators
    // Whatever members are neccessary to maintain the data.
    // Probably only a single shared_ptr and some indices
};

struct image : const_image {
    // delegating ctors for all but copy-construction and move-constructor
    // copy-ctor accepting a non-const image, probably defaulted
    // move-constructor, (move-)assignment-operators
    // additional non-const inspectors (where possible a thin mask over the const ones)
    // these all return Image or manipulatable internal state
    // manipulators
private:
    // if neccessary, optional additional state for supporting manipulation-operations
};
    
risposta data 06.10.2015 - 02:29
fonte
1

TL; DR

  1. (Idea personale, non dimostrata)
    • "Questo cptr è const , che mptr non è
  2. Un'idea quasi provata, in aggiunta al design Mat_<T> di OpenCV
    • Cuocere le costanti nel parametro del modello T e
    • Assicurati che tutti i buchi siano tappati, apportando alcune modifiche a OpenCV.

Questo è difficile e uno che anche le ben note librerie di immagini come OpenCV non trovano una soluzione perfetta. Invece la maggior parte della gente vivrebbe con pragmatismo, cioè sii soddisfatto di quello che ha.

Ecco la mia idea non provata:

  • In ogni classe Image (e anche Buffer ), mantieni due campi del puntatore non elaborati:
    • Uno è un puntatore non const const (o uchar ).
    • Uno è un puntatore const void (o uchar ). (Significa che non consente di modificare i dati puntati, tramite questo puntatore.)
  • Nelle istanze Image che devono "applicare la costanza", anche se qualcuno è riuscito a ottenere un riferimento non const-qualificato a quel Image , il puntatore non-const è intenzionalmente impostato su nullptr .
    • Prima di apportare modifiche ai pixel, qualcuno dovrà convalidare il puntatore prima di procedere. In genere, se si intende modificare un intero gruppo di pixel, è sufficiente convalidare il puntatore una sola volta, quindi con una progettazione corretta questo non dovrebbe diventare un overhead delle prestazioni.
    • Tutte le modifiche verranno eseguite tramite il puntatore non-const e tutte le operazioni di sola lettura verranno eseguite tramite il puntatore const.
    • Per le immagini mutevoli (o le immagini che consentono modifiche), i due puntatori avranno sempre lo stesso valore.

Questa è solo un'idea selvaggia e non provata. Sentiti libero di esplorare e criticare. Detto questo, la mia idea viene prodotta da un uso prolungato di OpenCV e implementazioni di elaborazione di immagini di basso livello.

Come bonus, se hai già familiarità con OpenCV, puoi provare le seguenti due dichiarazioni:

const auto sz = cv::Size(3, 3); const uchar fillval = 0U; cv::Mat_<uchar> matMutable1B(sz, fillval); cv::Mat_<const uchar> matImmutable1B(sz, fillval);

Si noti che il primo consente modifiche tramite il solito stile OpenCV, ad es. mat.at(row, col) = fillval; , mat(row, col) = fillval; , mat.ptr(row)[col] = fillval; , mentre il secondo non lo consente.

Ciò è dovuto al fatto che il parametro template <T> è stato qualificato const dalla seconda dichiarazione.

Tuttavia, al momento, questo non è infallibile, perché qualcuno che ottiene il puntatore tramite mat.data otterrà un uchar* , indipendentemente dalla qualifica costante del parametro template <T> .

Una distinzione molto importante su immutability / object state access control contro C++ style notational const-qualification

La cost-qualificazione del C ++ influenza il codice che "trova / ottiene" un riferimento a una certa classe. Quindi, passa lungo la qualifica costante di qualcosa come una catena di custodia di tipi. Il Image di per sé non può scoprire se qualcuno che possiede un riferimento ad esso e fare una chiamata ad esso è const-qualificato o no. Invece, C ++ applica questo quando si controlla il codice del chiamante e si blocca il tentativo del chiamante. La classe Image non lo sa mai.

La mia idea di usare due puntatori (const / non-const) e impostare il non-const su nullptr è un tentativo di risolvere questo problema in fase di runtime. Quindi, qualcuno che ottiene un Image che non è qualificato const, ma il suo puntatore di dati non-const è nullptr, dovrà affrontare conseguenze di runtime.

In definitiva, se questo è troppo difficile da discutere, forse basta trascorrere un po 'di tempo con OpenCV, e provare sia la qualifica della matrice stessa che il parametro template, per vedere quale tecnica soddisfa la maggior parte delle tue esigenze.

Un'altra lezione appresa da OpenCV è che devi implementare il tuo meccanismo di conteggio dei riferimenti nella classe dell'immagine. In caso contrario, il codice dell'utente della biblioteca è molto fragile.

    
risposta data 02.10.2015 - 07:47
fonte
1

Sembra che io sia un po 'in ritardo per la festa, ma spero che l'OP veda questa risposta. La migliore soluzione al tuo problema non è usare l'ereditarietà, ma piuttosto lasciare la classe Image così com'è. Invece, ciò di cui hai bisogno sono due classi Alias: Alias e ConstAlias. Quindi, cambia tutte le tue funzioni che non hanno realmente bisogno dell'immagine, ma un alias per prendere Alias o ConstAlias come appropriato. Il metodo alias ha una versione const e non const, che restituiscono rispettivamente i tipi ConstAlias e Alias.

Questo suona familiare da qualche parte? Sostituisci immagine con vettore e Alias con iteratore e hai ... beh, metà STL.

È meglio distinguere esplicitamente tra Immagini e Alias usando il sistema dei tipi, il primo è un proprietario e quest'ultimo non lo è. La maggior parte delle funzionalità richiede solo una vista (possibilmente mutevole) dei dati, non ha bisogno di pasticciare effettivamente con la proprietà. Questo ti fa pensare più a lungo se la tua funzione dovrebbe prendere un'immagine o solo un alias. Permette anche di evitare l'uso di shared_ptr e mantenere un modello di proprietà molto più semplice e chiaro (e possibilmente più performante).

    
risposta data 12.10.2015 - 05:08
fonte
0

Penso che sarebbe meglio affrontare il problema da una prospettiva leggermente diversa.

C ++ funziona in molti modi meglio con tipi che si comportano come tipi di valore. Cioè, si comportano come tipi built-in o la maggior parte dei tipi di STL. Potresti creare una classe Image che si comporta come std::vector ed è un tipo di valore con correttezza const "profonda" semplicemente copiando i dati ogni volta che copi un'immagine o prendi una porzione di immagine, ma ciò sarebbe in qualche modo inefficiente a causa di la copia. Quella è la semantica che ti piacerebbe idealmente dalla tua classe Image .

Dovresti pensare alla condivisione dei dati delle immagini per l'efficienza come puramente un dettaglio di implementazione della tua classe. Non è qualcosa che l'utente finale dovrebbe avere bisogno di sapere da un punto di vista dell'interfaccia. Un modo per ottenere ciò è utilizzare la funzione copy-on-write per dare l'aspetto di un tipo di valore mentre sotto la cappa si condividono i dati laddove possibile. Questa era una tattica di implementazione comune per std::string anche se non lo è più, principalmente a causa di problemi relativi alla sicurezza dei thread (che dovrai affrontare anche se vuoi utilizzare la tua classe Image negli scenari multithread) .

Una delle implicazioni di questo approccio alternativo è che alias() è un brutto nome per la tua funzione slice image. Espone un dettaglio di implementazione nel nome di un'API pubblica. Quella funzione dovrebbe invece essere chiamata slice() . Con un'implementazione copy-on-write puoi chiamare slice() su const Image e ottenere un Image . Dietro le quinte i dati delle immagini sarebbero condivisi fino a quando non avresti provato a scriverci, a quel punto ne avresti fatto una copia. Un'implementazione più sofisticata potrebbe affiancare l'immagine e copiare solo le tessere che sono state toccate dalla scrittura, quindi cambiare alcuni pixel non richiederebbe una copia dell'intera fetta dell'immagine.

    
risposta data 05.10.2015 - 19:08
fonte
0

This solves the above mentioned problem (1). However if you look at the inheritance relation as an "is a" statement then "a mutable image IS AN immutable image" makes no sense. Which speaks against using inheritance here.

Bene ... un'immagine mutevole IS un'immagine immutabile, con capacità extra (per la mutazione) - il che ha molto senso (proprio come un chirurgo è un medico con capacità extra). Il fatto che le capacità extra siano una distinzione tra mutabile e immutabile fa poca differenza (è comunque un'immagine immutabile con un comportamento mutevole aggiunto).

Immagino la confusione che ha creato la domanda, è il fatto che nel linguaggio parlato, non ha senso dire "una X mutabile è una X immutabile" (perché nel pensiero razionale, A o non-A, ma non entrambi).

Questa relazione OO "è-a", però, non è proprio la stessa relazione "è-a" che hai in inglese parlato.

    
risposta data 06.10.2015 - 11:41
fonte
-1

Il problema fondamentale è che stai cercando di utilizzare un modello di tipi non valido. Nonostante la formulazione dello standard ISO C ++ e le idee errate espresse da Bjarne nell'ARM originariamente, non esiste un tipo const. Al contrario, la costanza è una proprietà solo dei puntatori (e dei riferimenti, ma lasciali uscire per semplicità). Pertanto una classe che rappresenta un tipo const e non const è un concetto errato.

Const in C ++ è un concetto non valido, dovrebbe essere stato rimosso. Ciò è evidente nella Libreria standard quando si cerca di modellare il concetto in modo generico. Funziona solo a un livello profondo come hai osservato. Si applica al percorso di accesso a un oggetto, ma non si propaga in una struttura di dati (raccolta di oggetti collegati). L'esempio più semplice è una lista concatenata: un puntatore const su un nodo lista produce immediatamente un puntatore successivo non const.

In un modello teorico, le astrazioni const e non const sono caratterizzate dall'assenza o dalla presenza di metodi mutator e sono quindi tipi distinti senza alcuna relazione "isA", qualunque essa sia.

Il mio miglior consiglio: smetti di provare a modellare le astrazioni con le tecniche OO perché OO ha dimostrato di essere così limitato nell'applicabilità che non è adatto per modellare alcuno ma i concetti più semplici. Impara invece alcuni metodi di programmazione funzionale.

    
risposta data 05.10.2015 - 18:20
fonte

Leggi altre domande sui tag