Newsgroup: it.comp.lang.c++
Titolo: Estendere tipi al valore nullo

Ciao a tutti,
visto che il newsgroup e' tornato ad essere piu' o meno un mortorio vi
butto li' un altro esercizietto.
Un esigenza che nasce naturalmente quando si lavora con database
relazionali, ma che a mio avviso e' di applicabilita' ben piu' generale
e' la possibilita' di definire un tipo che assuma tutti i valori di un
tipo base piu' il valore nullo. Voi come fareste?
Non vi do neanche una traccia, un po' perche' e' un problema facile, un
po' perche' esistono soluzioni diverse. Penso pero' che sarebbe meglio
cominciare col discutere delle scelte, sia a livello di interfaccia che
a livello di implementazione, prima di passare all'implementazione.
Ciao,
Nicola
P.S. I piu' anziani si ricorderanno che questo tema e' gia' stato
discusso anni fa. Magari ora siamo piu' vecchi e piu' saggi :-)

 tipo base piu' il valore nullo. Voi come fareste?
Questo è un po' il problema della divisione per zero o infinito su infinito.
Ci sono diverse teorie, ognuno può dire la sua!
Se la struttura dati è memorizzata su file sarebbe possibile utilizzare una
bitmap che dichiara i campi null da quelli validi. Allo stesso modo gli oggetti
internamente avranno un bit/booleano.
Questa soluzione è preferibile, perchè a volte non è possibile trovare un valore
per il tipo che è possibile considerare invalido.
Ad esempio per un tipo string "NULL" o "" possono essere valori validi, allo
stesso modo se il tipo è un intero, non è detto che "0" debba essere un valore
riservato.
Nella programmazione OO questa proposta può fare uso delle eccezioni per evitare
che qualcuno esegua un get su un campo nullo, in modo alternativo alla semplice
restituzione di puntatori null, che costringerebbe lo sviluppatore a dover
utilizzare quasi esclusivamente oggetti dinamici. Naturalmente se si sceglie
questa politica è necessario dotare l'oggetto dati dei metodi isNull per i vari
campi.

 
Credo che la cosa migliore sarebbe quella di usare un wrapper template 
che contenga un membro del tipo wrappato e un membro bool per tener 
traccia del valore null.
In pratica il Nullable<> del .net
Il costruttore di default imposta il campo a null, poi serve almeno un 
altro costruttore che accetta il valore da assegnare
Poi serve un accessor get che tira un'eccezione se il campo è null.
Idem per un operatore di conversione al tipo wrappato.
Può anche essere utile (sempre prendendo spunto dal .net) un
T GetValueOrDefault(T defaultVal)
che ritorna il valore wrappato (se valorizzato) o il valore di default 
passato per argomento senza mai tirare eccezioni
Poi dobbiamo definire come si comportano i confronti:
null vs valore
null vs null
valore vs null
Unica pecca:
il Nullable è perfetto der i tipi "nativi", potrebbe avere prestazioni 
scadenti con tipi complessi come string.
Si potrebbe pensare ad usare i riferimenti e la const per ottimizzare un 
po di copie inutili, oppure provare a definire esplicitamente il 
template per il caso string (ma qui mi fermo e non ho tempo neanche per 
pensarci)
My 50 cents
BB

 a livello di implementazione, prima di passare all'implementazione.
Purtroppo il C++ pone varie restrizioni su quello che si può fare.
E' molto facile propendere per un'interfaccia irrealizzabile.
A livello teorico, l'aggiunta di un elemento estraneo comporta notevoli 
problemi. Per es. non si possono avere interi con due elementi neutri 
per la somma e preservare tutte le proprietà.
Nel codice che segue, il nuovo elemento si chiama 'zero' ed è 
responsabilità dell'utente non utilizzare l'oggetto quando vale 'zero'. 
Sebbene sia possibile aggiungere controlli parziali, non penso ne valga 
la pena.
---
#include <iostream>
using namespace std;
struct Zero {} zero;
typedef char no;
typedef char (&yes)[2];
template <class T>
yes couldHaveMembers(int T::*);
template <class T>
no couldHaveMembers(...);
template <class T, int s = sizeof(couldHaveMembers<T>(0))>
class Wrapper;
template <class T>
class Wrapper<T, sizeof(yes)> : public T
{
	bool zero;
public:
	Wrapper() : T(), zero(true) {}
	Wrapper(Zero) : zero(true) { T::operator=(T()); }
	Wrapper(const T& t) : T(t), zero(false) {}
	T& operator=(const T& tt) { T::operator=(tt); zero = false; return *this; }
	operator T&() { if (zero) throw; return t; }
	bool operator!=(Zero) const { return !zero; }
	template <class U>
	bool operator==(U t) { return (bool)T::operator==(t); }
	template <>
	bool operator==<Zero>(Zero) { return zero; }
	template <class U>
	bool operator!=(U t) { return (bool)T::operator!=(t); }
	template <>
	bool operator!=<Zero>(Zero) { return zero; }
};
template <class T>
class Wrapper<T, sizeof(no)>
{
	T t;
	bool zero;
public:
	Wrapper() : t(), zero(true) {}
	Wrapper(Zero) : t(), zero(true) {}
	Wrapper(const T& t) : t(t), zero(false) {}
	operator T&() { if (zero) throw; return t; }
	bool operator==(Zero) const { return zero; }
	bool operator!=(Zero) const { return !zero; }
};
struct A
{
	bool operator==(const A&) { return 1; }
	bool operator==(int) { return 1; }
	bool operator!=(const A&) { return 1; }
};
int main()
{
	Wrapper<int> i = 1, j;
//	double d = j+i;	++d;		// errore: j non inizializzato!
	i = zero;
	if (i == zero)
		cout << "ok1\n";
	i = 2;
	++i;
	if (i >= 3)
		cout << "ok2\n";
	Wrapper<string> s = "not ok1";
	s = "ok3";
	cout << s.c_str() << endl;
	s = zero;
	if (s == zero)
		s = "ok4";
	else
		s = "not ok2";
	cout << s.c_str() << endl;
	Wrapper<A> a1, a2;
	a1 == a2;
	a1 == 3;
}
---
Kiuhnm

     bool operator!=<Zero>(Zero) { return zero; }
Ovviamente manca un '!':
bool operator!=<Zero>(Zero) { return !zero; }
Kiuhnm

 a livello di implementazione, prima di passare all'implementazione.
come vedi
boost::variant<T, nullptr_t> ?
... peccato che nullptr_t sia di C++0x, credo comunque lo si possa 
simulare per il momento con una struct vuota.
--
Marco

 campi.
Attenzione che si vuole rappresentare il valore nullo come estensione
del range di un tipo, non gestire la presenza di valori nulli nei
membri dati di una struttura.
Ciao,
Nicola

 pensarci)
Beh, salvo il fatto di usare il passaggio di parametri per const
reference dove lo si fa di solito il concetto di "annullabile" e'
tipico dei tipi di dati che hanno la semantica dei valori e l'uso della
copia e' abbastanza normale.
Ciao,
Nicola

 {
L'uso della classe base e' fragile perche' non e' detto che sia dotata
di distruttore virtuale, anche se generalmente questi tipi di dati
vengono passati per valore. Per contro consentirebbe di mantenere il
piu' possibile la semantica originale del tipo base aggiungendo
opportuni costruttori template.
Anche la tua soluzione ha il (possibile) difetto di richiedere che il
tipo base abbia il costruttore di default.
Ciao,
Nicola

 tipo base abbia il costruttore di default.
Be', non è indispensabile.
Kiuhnm

 membri dati di una struttura.
Si vero, ho utilizzato come esempio una struttura complessa perchè mi era venuto
in mente come spesso vengono gestite le letture di record di una tabella in un
database relazionale dove esistono le funzioni per conoscere se una determinata
colonna di un record abbia valori nulli o meno.
Rimane il fatto che il linguaggio non supporta nativamente la concezione di
nullità, quindi dovrà comunque esistere una struttura dati che contiene un
valore e un flag per la nullità dell'oggetto.
Diversamente, java ed altri hanno tale supporto (parziale) visto che è possibile
rendere nullo un oggetto. Ho scritto parziale perchè è il puntatore a diventare
nullo mentre l'oggetto continuerà a vivere se referenziato da altre variabili e
perchè i tipi primitivi (int, char ecc ecc) non possono resi null.

 tutte le operazioni usuali:
In effetti si potrebbe benissimo porlo uguale a 0xffffffff o farlo 
puntare a una variabile chiamata INVALID.
Kiuhnm

 tipo base abbia il costruttore di default.
Ho trovato mezz'ora per provare alcune modifiche:
---
template <class T>
class Wrapper<T, sizeof(yes)> : private T
{
	bool zero;
public:
	Wrapper(const Wrapper& w) : T(w), zero(w.zero) {}
	Wrapper() : T(), zero(true) {}
	template <class U1, class U2>
	Wrapper(const U1& u1, const U2& u2) : T(u1,u2), zero(false) {}
	template <class U1, class U2, class U3>
	Wrapper(const U1& u1, const U2& u2, const U3& u3) : T(u1,u2,u3), 
zero(false) {}
	// ecc...
	Wrapper(Zero) : T(), zero(true) {}
	template <class U1>
	Wrapper(Zero, const U1& u1) : T(u1), zero(true) {}
	template <class U1, class U2>
	Wrapper(Zero, const U1& u1, const U2& u2) : T(u1,u2), zero(true) {}
	template <class U1, class U2, class U3>
	Wrapper(Zero, const U1& u1, const U2& u2, const U3& u3) : T(u1,u2,u3), 
zero(true) {}
	// ecc...
	Wrapper(const T& t) : T(t), zero(false) {}
	Wrapper& operator=(const T& tt) { T::operator=(tt); zero = false; 
return *this; }
	Wrapper& operator=(Zero) { zero = true; return *this; }
	operator T&() { if (zero) throw; return t; }
	bool operator!=(Zero) const { return !zero; }
	template <class U>
	bool operator==(U t) { return (bool)T::operator==(t); }
	template <>
	bool operator==<Zero>(Zero) { return zero; }
	template <class U>
	bool operator!=(U t) { return (bool)T::operator!=(t); }
	template <>
	bool operator!=<Zero>(Zero) { return !zero; }
	T* operator->() { if (zero) throw; return this; }
};
---
In questo modo si può avere
---
class A
{
	A() {}
public:
	A(int,int) {}
	bool operator==(const A&) const { return 1; }
	bool operator==(int) const { return 1; }
	bool operator!=(const A&) const { return 1; }
};
Wrapper<A> a1(zero,6,9), a2(9,4);
---
All'utente non piace? Non deve fare altro che aggiungere un costruttore 
di default, oppure scrivere
Wrapper<A> a1(6,9), a2(9,4);
a1 = zero;
Questo significa però che "a1 = zero" non libera memoria. Si può ovviare 
usando un hasDefConstr a aggiungendo un po' di codice.
Naturalmente, si deve anche usare '->' al posto di '.'.
I vari throw possono essere relegati alla versione Debug.
Kiuhnm

 perchè i tipi primitivi (int, char ecc ecc) non possono resi null.
Come dicevo prima in C++ questa puo' essere una tecnica di
implementazione.
Ciao,
Nicola Musatti

 butto li' un altro esercizietto.
Speravo di attirare una discussione piu' vivace, ma dal momento che
l'argomento mi pare esaurito butto li' la mia risposta.
Sul piano concettuale mi pare che esistano due approcci distinti:
quello piu' calzante con il problema da me posto e' effettivamente la
teorica estensione del range del tipo base; un approccio alternativo e'
quello usato da Boost.Optional, che e' sostanzialmente un contenitore
di zero o piu' elementi.
Nel primo caso tipicamente si accede per conversione al tipo base,
mentre Boost.Optional richiede una dereferenziazione esplicita.
Sul piano implementativo la decisione fondamentale e' se l'elemento del
tipo base e' contenuto direttamente, incorendo cosi' nella necessita'
della costruzione di default, o indirettamente. In questo caso
l'oggetto puo' contenere un buffer di dimensioni e allineamento
opportuni nel quale l'elemento viene costruito solo se necessario.
Un'ulteriore alternativa e' quella di usare un puntatore, stupido o
intelligente. Se il tipo base e' grosso e raramente presente potrebbe
essere una soluzione adeguata.
Quest'ultima variante risolve direttamente anche il problema della
rappresentazione della presenza/assenza del valore. Le alternative non
sono molte: o si riserva un valore valido come null convenzionale o si
usa un booleano. Nella mia implementazione uso il meccanismo dei traits
per scegliere tra le due alternative. Come suggerisce PAN Boost.Variant
e' un'ulteriore alternativa.
Il cuore della mia implementazione, che risale a qualche anno fa, era:
template <typename ValueType, bool specialized>
class NullHolder
{
    public:
        NullHolder() {}
        NullHolder(const NullType &) : null(true) {}
        NullHolder(const ValueType & v) : val(v), null(false) {}
        void assign(const ValueType & data) { val = data; null = false;
}
        void assign(const NullType &) { null = true; }
        bool isNull() const { return null; }
        ValueType get() const { return val; }
        bool equals(const NullHolder & v) const
        {
            return null == v.null && ( null || val == v.val );
        };
    private:
        ValueType val;
        bool null;
};
template <typename ValueType>
class NullHolder<ValueType, true>
{
        typedef NullTraits<ValueType> TraitsType;
    public:
        NullHolder() {}
        NullHolder(const NullType &) : val(TraitsType::value()) {}
        NullHolder(const ValueType & v) : val(v) {}
        void assign(const ValueType & data) { val = data; }
        void assign(const NullType &) { val = TraitsType::value(); }
        bool isNull() const { return val == TraitsType::value(); }
        ValueType get() const { return val; }
        bool equals(const NullHolder & v) const { return val == v.val;
}
    private:
        ValueType val;
};
Ciao,
Nicola