2015-12-15 13 views
20

Nel nostro progetto, usiamo un bel po 'di "usanze" per indicare esplicitamente cosa si suppone che la variabile rappresenti. Viene utilizzato principalmente per gli identificatori std::string come PortalId o CakeId. Ora quello che possiamo fare attualmente èC++ Fortemente digitato usando e typedef

using PortalId = std::string; 
using CakeId = std::string; 

PortalId portal_id("2"); 
CakeId cake_id("is a lie"); 

portal_id = cake_id; // OK 

che non ci piace. Vorremmo avere un controllo di tipo durante la compilazione per impedirci di mescolare mele e arance preservando la maggior parte dei metodi yum yum dall'oggetto originale.

Quindi la domanda è: è possibile farlo in C++ in modo tale che l'utilizzo sia vicino a quanto segue, i compiti non funzionerebbero e potremmo ancora usarlo nelle mappe e in altre cose?

SAFE_TYPEDEF(std::string, PortalId); 
SAFE_TYPEDEF(std::string, CakeId); 

int main() 
{ 
    PortalId portal_id("2"); 
    CakeId cake_id("is a lie"); 
    std::map<CakeId, PortalId> p_to_cake; // OK 

    p_to_cake[cake_id] = portal_id; // OK 
    p_to_cake[portal_id] = cake_id; // COMPILER ERROR 

    portal_id = cake_id;  // COMPILER ERROR 
    portal_id = "1.0";   // COMPILER ERROR 
    portal_id = PortalId("42"); // OK 
    return 0; 

} 

Abbiamo già provato alcune macro in combinazione con i modelli, ma non abbiamo ottenuto ciò di cui avevamo bisogno. E per aggiungere - possiamo usare C++ 14.

EDIT: Il codice ci siamo inventati era

#define SAFE_TYPEDEF(Base, name) \ 
class name : public Base { \ 
public: \ 
    template <class... Args> \ 
    explicit name (Args... args) : Base(args...) {} \ 
    const Base& raw() const { return *this; } \ 
}; 

che è brutto e non funziona. E non funziona, voglio dire che il compilatore era ok con portal_id = cake_id; .

EDIT2: Aggiunto explicit parola chiave, con il quale il nostro codice funziona davvero bene per esempio. Non sono sicuro se questa sia la strada giusta da percorrere e se riguardi tutte le situazioni sfortunate.

+0

Beh niente :-D Mi impegno a fornire il codice. – Jendas

+1

Basta aggiungere esplicitamente prima di c-tor. – firescreamer

+0

Se lo si fa per le stringhe, l'uso del tipo può facilmente portare a un comportamento indefinito: 'SAFE_TYPEDEF (std :: string, S); std :: string * s = new S(); cancella s; '' std :: string' non è pensato per essere usato come classe base. – Jens

risposta

13

Ecco una soluzione completa minimo che farà quello che vuoi.

È possibile aggiungere più operatori ecc. Per rendere la classe più utile come meglio credi.

#include <iostream> 
#include <string> 
#include <map> 

// define some tags to create uniqueness 
struct portal_tag {}; 
struct cake_tag {}; 

// a string-like identifier that is typed on a tag type 
template<class Tag> 
struct string_id 
{ 
    // needs to be default-constuctable because of use in map[] below 
    string_id(std::string s) : _value(std::move(s)) {} 
    string_id() : _value() {} 

    // provide access to the underlying string value   
    const std::string& value() const { return _value; } 
private: 
    std::string _value; 

    // will only compare against same type of id. 
    friend bool operator < (const string_id& l, const string_id& r) { 
     return l._value < r._value; 
    } 
}; 


// create some type aliases for ease of use  
using PortalId = string_id<portal_tag>; 
using CakeId = string_id<cake_tag>; 

using namespace std; 

// confirm that requirements are met 
auto main() -> int 
{ 
    PortalId portal_id("2"); 
    CakeId cake_id("is a lie"); 
    std::map<CakeId, PortalId> p_to_cake; // OK 

    p_to_cake[cake_id] = portal_id; // OK 
// p_to_cake[portal_id] = cake_id; // COMPILER ERROR 

// portal_id = cake_id;  // COMPILER ERROR 
// portal_id = "1.0";   // COMPILER ERROR 
    portal_id = PortalId("42"); // OK 
    return 0; 
} 

ecco una versione aggiornata che gestisce anche le mappe hash, lo streaming di ostream ecc

Noterete che non ho fornito un operatore di convertire in string. Questo è intenzionale. Sto richiedendo che gli utenti di questa classe esprimano esplicitamente l'intenzione di usarlo come stringa fornendo un sovraccarico di to_string.

#include <iostream> 
#include <string> 
#include <map> 
#include <unordered_map> 

// define some tags to create uniqueness 
struct portal_tag {}; 
struct cake_tag {}; 

// a string-like identifier that is typed on a tag type 
template<class Tag> 
struct string_id 
{ 
    using tag_type = Tag; 

    // needs to be default-constuctable because of use in map[] below 
    string_id(std::string s) : _value(std::move(s)) {} 
    string_id() : _value() {} 

    // provide access to the underlying string value 
    const std::string& value() const { return _value; } 
private: 
    std::string _value; 

    // will only compare against same type of id. 
    friend bool operator < (const string_id& l, const string_id& r) { 
     return l._value < r._value; 
    } 

    friend bool operator == (const string_id& l, const string_id& r) { 
     return l._value == r._value; 
    } 

    // and let's go ahead and provide expected free functions 
    friend 
    auto to_string(const string_id& r) 
    -> const std::string& 
    { 
     return r._value; 
    } 

    friend 
    auto operator << (std::ostream& os, const string_id& sid) 
    -> std::ostream& 
    { 
     return os << sid.value(); 
    } 

    friend 
    std::size_t hash_code(const string_id& sid) 
    { 
     std::size_t seed = typeid(tag_type).hash_code(); 
     seed ^= std::hash<std::string>()(sid._value); 
     return seed; 
    } 

}; 

// let's make it hashable 

namespace std { 
    template<class Tag> 
    struct hash<string_id<Tag>> 
    { 
     using argument_type = string_id<Tag>; 
     using result_type = std::size_t; 

     result_type operator()(const argument_type& arg) const { 
      return hash_code(arg); 
     } 
    }; 
} 


// create some type aliases for ease of use 
using PortalId = string_id<portal_tag>; 
using CakeId = string_id<cake_tag>; 

using namespace std; 

// confirm that requirements are met 
auto main() -> int 
{ 
    PortalId portal_id("2"); 
    CakeId cake_id("is a lie"); 
    std::map<CakeId, PortalId> p_to_cake; // OK 

    p_to_cake[cake_id] = portal_id; // OK 
    // p_to_cake[portal_id] = cake_id; // COMPILER ERROR 

    // portal_id = cake_id;  // COMPILER ERROR 
    // portal_id = "1.0";   // COMPILER ERROR 
    portal_id = PortalId("42"); // OK 

    // extra checks 

    std::unordered_map<CakeId, PortalId> hashed_ptocake; 
    hashed_ptocake.emplace(CakeId("foo"), PortalId("bar")); 
    hashed_ptocake.emplace(CakeId("baz"), PortalId("bar2")); 

    for(const auto& entry : hashed_ptocake) { 
     cout << entry.first << " = " << entry.second << '\n'; 

     // exercise string conversion 
     auto s = to_string(entry.first) + " maps to " + to_string(entry.second); 
     cout << s << '\n'; 
    } 

    // if I really want to copy the values of dissimilar types I can express it: 

    const CakeId cake1("a cake ident"); 
    auto convert = PortalId(to_string(cake1)); 

    cout << "this portal is called '" << convert << "', just like the cake called '" << cake1 << "'\n"; 


    return 0; 
} 
+0

Eccellente idea libro di testo. – Elyasin

+1

La tua soluzione è ottima, ma vorrei aggiungere un secondo parametro template per il tipo di '_value': http: //ideone.com/IVlefk – dkg

+0

ancora meglio, sì. Quindi potresti iniziare a fare delle specializzazioni di classe value_traits per coprire cose come la generazione automatica di ID univoci ecc. Ecc. –

1

Sarebbe bello se ci fosse un modo standard per farlo, ma attualmente non c'è. Qualcosa potrebbe essere standardizzato in futuro: c'è un documento su Opaque Typedefs che tenta di farlo con alias funzione e un costrutto di eredità più ricco e uno su Named Types che richiede un approccio molto più semplice con una singola nuova parola chiave per introdurre un typedef forte, o qualsiasi altra cosa vuoi chiamarlo

La libreria di serializzazione Boost fornisce BOOST_STRONG_TYPEDEF che potrebbe fornire ciò che si desidera.

Ecco un rimpiazzo per il vostro SAFE_TYPEDEF che è solo BOOST_STRONG_TYPEDEF senza altre dipendenze spinta e modificato leggermente in modo che non è possibile assegnare il tipo typedef d. Ho anche aggiunto un costruttore mossa e assegnazione e l'uso fatto di default:

namespace detail { 
    template <typename T> class empty_base {}; 
} 

template <class T, class U, class B = ::detail::empty_base<T> > 
struct less_than_comparable2 : B 
{ 
    friend bool operator<=(const T& x, const U& y) { return !(x > y); } 
    friend bool operator>=(const T& x, const U& y) { return !(x < y); } 
    friend bool operator>(const U& x, const T& y) { return y < x; } 
    friend bool operator<(const U& x, const T& y) { return y > x; } 
    friend bool operator<=(const U& x, const T& y) { return !(y < x); } 
    friend bool operator>=(const U& x, const T& y) { return !(y > x); } 
}; 

template <class T, class B = ::detail::empty_base<T> > 
struct less_than_comparable1 : B 
{ 
    friend bool operator>(const T& x, const T& y) { return y < x; } 
    friend bool operator<=(const T& x, const T& y) { return !(y < x); } 
    friend bool operator>=(const T& x, const T& y) { return !(x < y); } 
}; 

template <class T, class U, class B = ::detail::empty_base<T> > 
struct equality_comparable2 : B 
{ 
    friend bool operator==(const U& y, const T& x) { return x == y; } 
    friend bool operator!=(const U& y, const T& x) { return !(x == y); } 
    friend bool operator!=(const T& y, const U& x) { return !(y == x); } 
}; 

template <class T, class B = ::detail::empty_base<T> > 
struct equality_comparable1 : B 
{ 
    friend bool operator!=(const T& x, const T& y) { return !(x == y); } 
}; 

template <class T, class U, class B = ::detail::empty_base<T> > 
struct totally_ordered2 
    : less_than_comparable2<T, U 
    , equality_comparable2<T, U, B 
     > > {}; 

template <class T, class B = ::detail::empty_base<T> > 
struct totally_ordered1 
    : less_than_comparable1<T 
    , equality_comparable1<T, B 
     > > {}; 

#define SAFE_TYPEDEF(T, D)          \ 
struct D              \ 
    : totally_ordered1< D          \ 
    , totally_ordered2< D, T         \ 
    > >               \ 
{                \ 
    T t;              \ 
    explicit D(const T& t_) : t(t_) {};       \ 
    explicit D(T&& t_) : t(std::move(t_)) {};     \ 
    D() = default;            \ 
    D(const D & t_) = default;         \ 
    D(D&&) = default;           \ 
    D & operator=(const D & rhs) = default;      \ 
    D & operator=(D&&) = default;        \ 
    operator T &() { return t; }        \ 
    bool operator==(const D & rhs) const { return t == rhs.t; } \ 
    bool operator<(const D & rhs) const { return t < rhs.t; } \ 
}; 

Live Demo

+0

Ah, tipi nominati, era il termine che non riuscivo a ricordare. – Jendas

+0

Inoltre sono abbastanza sorpreso che i tipi denominati non siano già stati introdotti in C++ 14. – Jendas

+0

Indovinare i downvotes sono stati perché non ho fornito una soluzione completa? Ce n'è uno adesso. – TartanLlama

9

Le soluzioni fornite finora sembrano eccessivamente complesso: ecco la mia prova:

#include <string> 

enum string_id {PORTAL, CAKE}; 

template <int ID> class safe_str : public std::string { 
    public: 
    using std::string::string; 
}; 

using PortalId = safe_str<PORTAL>; 
using CakeId = safe_str<CAKE>; 
+0

Il problema con questo approccio è che i safe_str di diversi tipi sono entrambi derivati ​​dalla stringa pubblicamente. Sono quindi convertibili l'uno con l'altro. Ciò significa che safe_str non è affatto sicuro. –

+1

@RichardHodges è possibile convertirli, ma non come si farebbe accidentalmente. – kamikaze

+1

hai ragione. il costruttore di stringhe è esplicito. Bello. :) –

Problemi correlati