Classi “sealed” in C++…

Come promesso, finite le vacanze, rieccomi a postare sul mio piccolo blog.

Questa volta vedremo rapidamente come creare delle classi C++ che non si possono ulteriormente derivare. In altre parole, come simulare l’operatore sealed di C# in C++ (o almeno gli effetti).

Sigillare una classe in C++

Il “trucco” per creare una classe sigillata in C++ è noto da tempo: ne parla lo stesso stesso autore del C++, Stroustrup, sul suo sito.

L’idea che sta alla base è l’utilizzo combinato della friendship fra classi e della virtual inheritance.

La keyword friend permette ad una classe di garantire l’accesso alle sue variabili e funzioni membro ad un’altra classe, anche se le due classi non sono legate da alcun vincolo di parentela.

In altri termini, una classe A autorizza esplicitamente B ad operare sui suoi dati e con le sue funzioni interne, anche private/protected (cfr. questa pagina).

L’ereditarietà virtuale viene normalmente impiegata per risolvere i problemi di ambiguità con l’ereditarietà multipla. In questo caso tali problemi non sussistono ma la parola virtual torna utile proprio per impedire la derivazione.

Questioni di amicizia

Partiamo da una classe “base” di utilità A.

Questa classe definisce il suo costruttore privato e permette ad un’altra classe, B (tramite la keyword friend) di accedervi.

Ora come ora, solo B sarà in grado di derivare direttamente da A perchè è l’unica classe che può farlo, essendo stata autorizzata esplicitamente ad accedere al costruttore di A.

Se ora derivassimo da B una classe C senza particolari accorgimenti, non avremmo raggiunto l’obiettivo: C deriva da B che a sua volta deriva da A.

Il discorso funziona perchè, partendo da C, viene invocato il costruttore di B e poi, da quest’ultima, il costruttore di A.

Tutto regolare: non è C che provoca l’invocazione del costruttore di A, ma è B (che è stata autorizzata tramite keyword friend!).

Il ruolo dell’ereditarietà virtuale

Per rendere B non-derivabile, dobbiamo sfruttare l’ereditarietà virtuale (virtual inheritance).

Imponendo che B derivi da A in modo virtuale otteniamo un cambiamento nella gerarchia di derivazione ed inizializzazione: C deriva da B ma, a causa della keyword virtual, è come se fosse lei a dover accedere ad A.

Ma C, a differenza della sua genitrice B, non è stata esplicitamente autorizzata ad accedere ad A e quindi l’inizializzazione viene arrestata (la friendship non si eredita!).

Ora B è davvero “sealed“, ossia non derivabile.

Rendiamo le cose più maneggevoli

La parte noiosetta dell’applicare il trucco sta nel fatto che le classi da sigillare vanno tutte autorizzate esplicitamente tramite la keyword friend.

Per evitare ciò, si può creare una classe di utilità A “templated” che autorizzerà la classe che gli forniremo come parametro template.

Ovviamente tale classe sarà proprio la nostra classe B. :)

Il tutto è riassumibile in:

template <class T>
class A 
{ 
	private:
		friend class T; // <= at the moment, invalid code (it will be valid in C++0x)
		A() {}
}

class B: public virtual A<B> {}

Il giochetto per cui B deriva da A ed è anche parametro template di A è noto come Curiously Recurring Template Pattern (CRTP) e viene normalmente usato per aggiungere funzionalità ad una classe derivata (mixin from above”). Nel nostro caso serve a rendere B friend di A in modo “templated” (che bello sovvertire l’uso dei pattern a piacimento :P ).

Sfortunatamente lo standard C++ attuale non permette di dichiarare friend una classe template T (*) per cui bisognerà sfruttare un altro trucchetto (cfr. post): si avvolge la classe template T in una classe “wrapper” ed il gioco è fatto (cfr. Wrapper Pattern). Tale classe incapsula T e la espone come tipo interno. In questo modo la keyword friend non agirà direttamente su T (che è vietato) ma sul “tipo interno di una classe wrapper” (che è ancora T, sotto mentite spoglie però :D ).

Di seguito il codice di esempio completo.

Mi sono permesso di aggiungere una piccola macro, MarkUnheritable, che renderà il tutto (spero) più facile da leggere ed usare.

Il codice

/**
 *	FILE      : NoDerivation.h
 *	AUTHOR    : Gian Paolo "JP" Ghilardi (http://rejex.wordpress.com)
 *	LICENSE   : released under the terms of GPL v2.0 ("only")
 *	COMPILE   : g++ -Wall -Winline -pedantic NoDerivation.cpp -o NoDerivation
 *
 *	NOTE      : for more info, please see file NoDerivation.cpp
 */

#ifndef NO_DERIVATION_H_
#define NO_DERIVATION_H_

/*
   As the current C++ standard forbids "friend class T" with T templated, this
   is a practical workaround: it works by wrapping up the template class T
   and exposing T as an inner type [1].
 */
template <class T>
struct Wrapper
{
	typedef T inner_type;
};

/*
   Children of this class will be sealed that is they won't be derivable [2,3].
   This class is templated so we can obtain a sort of "templated friendship".
 */
template <class T>
class CannotInheritFromThisClass
{
	private:
		friend class Wrapper<T>::inner_type; // [1]  (use 'typename' instead of 'class' for VS2005)
		CannotInheritFromThisClass() { }
};

/*
   This macro does all the tricks...

   Because of virtual inheritance and because "friendship" is not inheritable,
   just a class deriving directly from CannotInheritFromThisClass() can access
   the constructor of this one.

   This makes that class non-derivable: an hypothetic child class should be able to access
   CannotInheritFromThisClass's ctor (virtual inheritance requires that) for correct
   initialization, but it couldn't because it's not friend with CannotInheritFromThisClass!
 */
#define MarkUnheritable(T) public virtual CannotInheritFromThisClass<T>

#endif // NO_DERIVATION_H_

/**
 *	FILE      : NoDerivation.cpp
 *	AUTHOR    : Gian Paolo "JP" Ghilardi (http://rejex.wordpress.com)
 *	LICENSE   : released under the terms of GPL v2.0 ("only")
 *	COMPILE   : g++ -Wall -Winline -pedantic NoDerivation.cpp -o NoDerivation
 *	PURPOSE   : simple C++ example showing how to create a non-derivable ("sealed") class.
 *
 *	TESTED ON :
 *	- Windows XP SP2, x86 32-bit, G++ 4.4.0 and VS2005
 *	- MacOSX 10.5.8, PPC 32-bit, G++ 4.4.1
 *
 *	REFERENCES:
 *	[1]: http://www.ddj.com/cpp/184403883
 *	[2]: http://www.research.att.com/~bs/bs_faq2.html#no-derivation
 *	[3]: http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.11
 *	[4]: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
 *	[5]: http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Curiously_Recurring_Template_Pattern
 *	[6]: http://cpptruths.blogspot.com/2006/01/curiously-recurring-template-pattern.html
 */

#include <cstdlib>
#include <iostream>
#include "NoDerivation.h"

using namespace std;

// Mark this class as sealed (notice: CRTP applied [4,5,6])
class SealedTest: MarkUnheritable(SealedTest) { };

// This class cannot derive from SealedTest!
class TryToInherit: public SealedTest { };

int main(int argc, char **argv)
{
	SealedTest st;    // OK
	TryToInherit tth; // ERROR
	return EXIT_SUCCESS;
}

L’esempio dal vivo

L’esempio sul Mac. Si noti l’errore provando ad estendere la classe SealedTest.

Example of sealed (non derivable) class in C++

(*): lo standard C++0x lo permetterà, invece (cfr. ultima bozza disponibile dello standard C++0x, §11.4(3)). Al momento il compilatore G++ non supporta ancora questa feature, indicata come “Extended friends declarations”

P.S.: Stroustrup nel suo esempio di classe non-derivabile utilizza “public virtual” (“is-a” relationship) mentre nell’esempio delle FAQ C++ Lite viene usato “private virtual” (“has-a” relationship). Visto il valore squisitamente didattico del mio esempio, che non richiede obbligatoriamente l’ereditarietà privata, ho seguito l’approccio di Stroustrup.

Contrassegnato da tag , , , , , , , ,

4 thoughts on “Classi “sealed” in C++…

  1. Stefano scrive:

    I patterns devono essere sovvertiti a proprio piacimento, altrimenti che gusto c’è ? ;)

    A proposito, non ho mai sopportato la keyword friend del C++, di solito quando la trovo nel codice sorgente di un progetto la vedo come ‘il seme del male’; una specie di ‘code smell’ …

  2. jp scrive:

    Ciao!

    A dire il vero nemmeno io ho mai apprezzato la keyword friend nè ho mai avuto necessità di usarla.

    In un certo senso, il fatto di dover dichiarare esplicitamente un legame di amicizia fra classi (che possono anche essere slegate, senza legami di parentela), IMHO, può aumentare il grado di coupling e/o comunque introduce un legame/dipendenza fra le classi coinvolte.

    Ad esempio, se si cambia il nome di una classe, bisogna modificare anche chi l’ha dichiarata friend perchè l’”amicizia” fra classi è un legame esplicito, ad personam. E’ vero, gli IDE al giorno d’oggi sistemano il tutto con pochi click del mouse (è un tipo di refactoring banalissimo) però questo non significa che si può abusarne a tutto spiano.

    Detto ciò, in questo caso il problema non sussiste: nonostante l’uso di friend, l’approccio CRTP non rende le classi meno loosely coupled (anche perchè le classi risultano comunque legate in una relazione padre-figlio).

    Alla peggio, se si abusa di questo template per classi sealed ed il compilatore impiegato non è particolarmente “sveglio”, si spreca un po’ di spazio per le varie instanziazioni dei template

    Ciao & grazie! ^^

    PS: parafrasando/parodiando Aleph One, “Smashing the Design Patterns for Fun and Profit”! :D

  3. Fabrizio scrive:

    Ogni tanto mi manca la “friend” in C#.
    Anzi, in realtà mi manca un controllo fine degli accessi ad un metodo o una proprietà. Non che manchino le alternative, ci sono mille modi per ovviare alla questione, in alcuni casi però ho lasciato pubblico un metodo che avrebbe potuto essere reso accessibile solo da una classe particolare.
    Per far accedere ai private di una classe vedo che spesso si usano le classi partiali.

    Ma se voglio che un metodo sia accedibile solo da una classe, o meglio un’interfaccia? Devo avere l’interfaccia come parametro, ovviamente. O probabilmente avere un riferimento ad una interfaccia… ecc.. ec..

    uff.. i soliti sofismi… sono le 8:30 e oggi devo macellare un microcontrollore. Il costrutto più evoluto che incontrerò è un array. Di puntatori.

  4. jp scrive:

    Ciao Zeno!

    In effetti il C# non ha un equivalente diretto. Forse la “friendship” non è stata considerata così fondamentale da meritare l’inclusione nel linguaggio…

    Direi che si può usare la keyword internal per simulare la “friendship”, però ti tocca mettere fisicamente le classi “amiche” in uno stesso assembly. Ad esempio nella stessa libreria DLL (cfr. questa domanda su StackOverflow)

    Ciao!

Lascia un Commento

Fill in your details below or click an icon to log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Log Out / Modifica )

Foto Twitter

You are commenting using your Twitter account. Log Out / Modifica )

Foto di Facebook

You are commenting using your Facebook account. Log Out / Modifica )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 259 other followers