Esempio di type traits (C++0x, reprise)…
Nel post precedente ho presentato un piccolo codice di esempio che permetteva di conoscere le caratteristiche, i “tratti” salienti di un tipo di dato, attraverso i type trait del nuovo standard C++.
In questo post ci spingeremo un poco più in là, sfruttandoli per creare una classe per confrontare in maniera sicura i vari tipi numerici di base del linguaggio.
Il problema
Sicuramente vi sarà capitato di dover confrontare fra loro due valori numerici.
Se i tipi in input sono interi (“integral types” nella nomenclatura C++) non ci sono problemi e basta usare il normale operatore di confronto (==) ma se se invece abbiamo a che fare con dei tipi floating-point?
Come è noto confrontare con l’operatore == due numeri floating-point è potenzialmente rischioso per come questi valori vengono codificati e memorizzati. In pratica, per ragioni di approssimazione, lo stesso numero floating-point può essere rappresentato con due codifiche lievemente diverse, tali da invalidare potenzialmente un “normale” confronto con quell’operatore.
Soluzione
Ecco quindi la nostra piccola funzione-esempio, compare(), che, sulla base del tipo in input (determinato tramite trait), seleziona il giusto modo per confrontare due valori: se si tratta di un intero, usiamo tranquillamente l’operatore == ma se dobbiamo confrontare due valori floating-point, usiamo un approccio più “sicuro”, basato sul cosiddetto valore machine epsilon.
Tale valore, dipendente dalla macchina, rappresenta lo scarto minimo per cui due numeri possono essere ritenuti uguali. In altri termini se confrontiamo lo stesso numero codificato però in due modi lievemente diversi, il valore machine epsilon rappresenta il massimo scarto (differenza) fra le due codifiche per cui possiamo considerare i due numeri uguali.
Si noti che è possibile calcolare un “machine epsilon” strettamente dipendente dal tipo di macchina che stiamo usando, ma tale valore viene fissato anche dall’implementazione/librerie C++ e a meno che non coincidano ha ovviamente precedenza quest’ultimo. E’ possibile recuperare comodamente il valore-costante epsilon per un dato tipo floating-point tramite std::numeric_limits.
Alcune note sul codice
Nella funzione main(), per ragioni di compattezza ho fatto abbondante uso di una caratteristica molto comoda del C++ nota come template argument deduction tramite cui lascio al compilatore la fatica di dover determinare i tipi degli input per i template, omettendoli in toto (uno degli esempi più noti di utilizzo è la funzione std::make_pair della Standard Template Library del C++).
La funzione compare() è inserita in un namespace marcato come inline: un’altra novità del nuovo standard C++0x.
Il codice è molto compatto (le righe “buone” sono solo due), debitamente commentato e spero che vi piaccia.
Ciau!
Codice d’esempio
/**
* FILE : TypeTraits.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 -std=c++0x
* TypeTraits.cpp -o TypeTraits
* PURPOSE : simple C++0x example showing new type traits facility.
*
* TESTED ON :
* - Windows XP, x86 32-bit, G++ 4.4.0
*
* REFERENCES:
* [1]: http://www.cplusplus.com/reference/std/limits/numeric_limits/
* [2]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2535.htm
* [3]: http://en.wikipedia.org/wiki/C%2B%2B0x#Rvalue_reference_and_move_semantics
* [4]: http://en.wikipedia.org/wiki/C%2B%2B0x#Static_assertions
* [5]: http://en.wikipedia.org/wiki/C%2B%2B0x#Type_traits_for_metaprogramming
* [6]: http://www.cplusplus.com/reference/clibrary/cmath/fabs/
* [7]: http://www.cplusplus.com/reference/iostream/manipulators/boolalpha/
* [8]: http://codeidol.com/cpp/cpp-templates/Template-Argument-Deduction/
*/
#ifdef __STRICT_ANSI__ // to avoid a MinGW G++ 4.4.0 bug ("swprintf undeclared...")
#undef __STRICT_ANSI__
#include <cstdlib>
#include <iomanip>
#include <iostream>
#define __STRICT_ANSI__
#else
#include <cstdlib>
#include <iomanip>
#include <iostream>
#endif
#include <limits>
#include <cmath>
#include <cstdlib>
#include <type_traits>
using namespace std;
/*
predefined "machine epsilon", a numeric value representing the
difference between 1 and the least value greater than 1 that
is representable [1]
Note: obviously just floating-point types have this value
*/
#define EPSILON(T) numeric_limits<T>::epsilon()
inline namespace SafeComp // inlining the namespace [2]
{
template <typename T>
inline static void compare(const T &&a, const T &&b) // rvalue refs [3]
{
// check if we're working on integral/floating-point types [4]...
static_assert(is_integral<T>::value || is_floating_point<T>::value,
"compare() only works on integral/floating-point types!");
/*
we use C++0x type traits [5] to decide how
to compare values: if types are integral,
we can compare them directly; otherwise we must
check floating-point "epsilon" value
Note #1: standard header <limits> [1] already contains "is_integer"
to check for integer types but the new type traits
include more checks even for numeric types
(i.e. "is_floating_point")
Note #2: fabs() is an (overloaded) function supporting
all C++ floating-point types [6]
*/
cout << fixed
<< setprecision(20)
<< boolalpha // boolean values printed as string [7]
<< " (" << a << " == " << b << ") => "
<< (is_integral<T>::value ? (a == b) : (fabs(b - a) < EPSILON(T)))
<< endl;
}
}
int main(int argc, char **argv)
{
cout << "\nC++0x EXAMPLE:" << endl;
cout << "Simple example showing C++0x type traits facility" << endl;
/*
Comparing some numeric values...
Note: template arguments are implicitly/automagically deducted [8]
Ex. : instead of writing SafeComp<double>::compare(1.0, 1.0)
we can just write SafeComp::compare(1.0, 1.0)
*/
cout << "\nComparing doubles: " << endl;
SafeComp::compare(1.0, 1.0); // true
SafeComp::compare(1.0, 2.0); // false
SafeComp::compare(1.0, 1.0 + EPSILON(double)); // false
cout << "\nComparing floats: " << endl;
SafeComp::compare(1.0f, 1.0f); // true
SafeComp::compare(1.0f, 2.0f); // false
SafeComp::compare(1.0f, 1.0f + EPSILON(float)); // false
cout << "\nComparing ints: " << endl;
SafeComp::compare(1, 1); // true
SafeComp::compare(1, 2); // false
return EXIT_SUCCESS;
}
Il codice in esecuzione
Questo è l’output del programma in esecuzione…


Ciao,
sai che sono un “bastian contrario” e non capisco le novità che in termini di funzionalità novità non sono.
come sempre (mi sono trattenuto fino ad ora
Nel caso specifico si poteva ottenere la stessa cosa con la specializzazione dei template e personalmente ritengo che fosse un modo più pulito e leggibile rispetto all’uso dei traits…
In realtà non riesco a trovare un esempio dove i traits risultano migliori delle tecniche classiche che il C++ mette a disposizione
cheers
Toh chi si rivede!
I traits, da quanto ne so, sono/possono essere implementati proprio come una serie di template + specializzazioni.
Siccome la gente li usava già (vedi Boost e TR1), erano già supportati da vari compilatori (e la loro implementazione ritenuta matura) e aggiungerli era tutto sommato semplice, li hanno inclusi nello standard.
Il fatto che siano inclusi nello standard, con un nome preciso, rende questa feature usabile ovunque e senza dover includere header esterni/non standard. Certa gente, per ragioni di portabilità, rifiuta di usare cose non presenti nello standard, a prescindere da tutto e tutti.
Inoltre, per come sono pensati, mantengono la “promessa C++” dello “zero overhead” perchè con le opportune ottimizzazioni attivate (e se non ti metti a giocare con cast dinamici o simili amenità) sono interamente gestiti a compile-time.
Diciamo che ti risparmiano del lavoro che altrimenti dovresti fare tu.
Ciao & grazie! ^^
> Diciamo che ti risparmiano del lavoro che altrimenti dovresti fare tu.
è su questa affermazione che non mi trovi d’accordo. Il lavoro tu l’hai fatto (nulla è gratis) e lo si vede anche nel tuo esempio: hai messo quell’if ( is_integral::value ? …)che ti permette di discernere fra i due casi (non indaghiamo ora se verrà o meno gestito a compile-time… su codici seri e complessi molto spesso non è così facile da predire
… e quello che io dico è che “quell’if mi sta un po’ qui” (si sono un sostenitore dell’eliminazione degli if e degli switch dalla programmazione… anche se non radicale
… visto che IMHO tende a mischiare qualcosa di “applicativo” (l’uguaglianza) e qualcosa di “architetturale” (il tipo di dato).
cheers
Anche io detesto gli if e se posso ne faccio volentieri a meno, però converrai con me che quanto ho scritto è un semplice esempio e che, nel caso specifico, è ragionevole supporre che un compilatore abbastanza astuto sia in grado non solo di elaborare tutto a compile-time ma pure di renderlo tutto inline…
Ciao! ^^