Dans notre projet, nous utilisons un grand nombre "d'utilisations" pour énoncer explicitement ce que la variable est censée représenter. Il est principalement utilisé pour std::string
identifiants comme PortalId
ou CakeId
. Maintenant, ce que nous pouvons faire actuellement, c'est
using PortalId = std::string;
using CakeId = std::string;
PortalId portal_id("2");
CakeId cake_id("is a lie");
portal_id = cake_id; // OK
que nous n'aimons pas. Nous aimerions avoir une vérification de type pendant la compilation pour nous empêcher de mélanger des pommes et des oranges tout en préservant la plupart des méthodes yum yum de l'objet d'origine.
La question est donc - cela peut-il être fait en C++ de telle sorte que l'utilisation soit proche de ce qui suit, les affectations échoueraient et nous pourrions toujours l'utiliser avec, par exemple, des cartes et d'autres conteneurs?
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;
}
Nous avons déjà essayé des macros en combinaison avec des modèles mais n'avons pas tout à fait obtenu ce dont nous avions besoin. Et pour ajouter - nous POUVONS utiliser c ++ 17.
EDIT: Le code que nous avons trouvé était
#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; } \
};
ce qui est moche et ne fonctionne pas. Et par cela ne fonctionne pas, je veux dire que le compilateur était d'accord avec .portal_id = cake_id;
EDIT2: Ajout du mot clé explicit
, avec lequel notre code fonctionne réellement bien pour notre exemple. Je ne sais pas si c'est la bonne façon de procéder et si cela couvre toutes les situations malheureuses.
Récemment, j'ai rencontré une bibliothèque appelée NamedTypes qui fournit du sucre syntaxique bien emballé pour faire exactement ce dont nous avions besoin! En utilisant la bibliothèque, notre exemple ressemblerait à ceci:
namespace fl = fluent;
using PortalId = fl::NamedType<std::string, struct PortalIdTag>;
using CakeId = fl::NamedType<std::string, struct CakeIdTag, fl::Comparable>;
int main()
{
PortalId portal_id("2");
CakeId cake_id("is a lie");
std::map<CakeId, PortalId> p_to_cake; // OK
p_to_cake.emplace(cake_id, portal_id); // OK
// p_to_cake.emplace(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;
}
NamedTypes
bibliothèque fournit beaucoup plus de propriétés supplémentaires comme Printable
, Incrementable
, Hashable
, etc. que vous pouvez utiliser pour créer par exemple indices fortement typés pour les tableaux et similaires. Voir le référentiel lié pour plus de détails.
Notez l'utilisation de la méthode .emplace(..)
, qui est nécessaire car le NamedType
n'est pas constructible par défaut, ce qui est requis par le []operator
.
Voici une solution complète minimale qui fera ce que vous voulez.
Vous pouvez ajouter plus d'opérateurs, etc. pour rendre la classe plus utile comme bon vous semble.
#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;
}
voici une version mise à jour qui gère également les cartes de hachage, le streaming vers ostream, etc.
Vous remarquerez que je n'ai pas fourni d'opérateur à convertir en string
. C'est délibéré. J'exige que les utilisateurs de cette classe expriment explicitement l'intention de l'utiliser comme chaîne en fournissant une surcharge de 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;
}
Les solutions fournies jusqu'à présent semblent trop complexes alors voici mon essai:
#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>;
Ce serait bien s'il y avait une façon standard de le faire, mais actuellement il n'y en a pas. Quelque chose pourrait être normalisé à l'avenir: il y a un document sur Opaque Typedefs qui essaie de le faire avec un alias de fonction et une construction d'héritage plus riche et un sur Types nommés qui prend un approche beaucoup plus simple avec un seul nouveau mot-clé pour introduire un typedef fort, ou comme vous voulez l'appeler.
La bibliothèque Boost Serialization fournit BOOST_STRONG_TYPEDEF
qui pourrait vous donner ce que vous voulez.
Voici un remplacement direct pour votre SAFE_TYPEDEF
qui est juste BOOST_STRONG_TYPEDEF
sans autres dépendances de boost et légèrement modifié pour que vous ne puissiez pas affecter à partir du type typedef
d. J'ai également ajouté un constructeur de mouvement et un opérateur d'affectation et j'ai utilisé 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; } \
};