Fuertemente tipado usando y typedef

12 minutos de lectura

Fuertemente tipado usando y typedef
Jendas

En nuestro proyecto, usamos bastantes “usos” para indicar explícitamente lo que se supone que representa la variable. Se utiliza principalmente para std::string identificadores como PortalId o CakeId. Ahora lo que podemos hacer actualmente es

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

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

portal_id = cake_id; // OK

que no nos gusta. Nos gustaría tener una verificación de tipo durante el tiempo de compilación para evitar que mezclemos manzanas y naranjas mientras preservamos la mayoría de los métodos yum yum del objeto original.

Entonces, la pregunta es: ¿se puede hacer esto en C ++ de manera que el uso sea similar a lo que sigue, las asignaciones fallarían y aún podríamos usarlo, por ejemplo, con mapas y otros contenedores?

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;

}

Ya probamos macros en combinación con plantillas, pero no obtuvimos lo que necesitábamos. Y para agregar, PODEMOS usar c ++ 17.

EDITAR: El código que se nos ocurrió fue

#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; } \
};

que es feo y no funciona Y por no funciona me refiero a que el compilador estaba bien con portal_id = cake_id;.

EDIT2: Agregado explicit palabra clave, con la que nuestro código realmente funciona bien para nuestro ejemplo. Sin embargo, no estoy seguro de si este es el camino correcto a seguir y si cubre todas las situaciones desafortunadas.

  • Simplemente agregue explícito antes de c-tor.

    – gritador de fuego

    15 de diciembre de 2015 a las 11:27

  • Si hace eso para las cadenas, usar el tipo puede conducir fácilmente a un comportamiento indefinido: SAFE_TYPEDEF(std::string, S); std::string* s = new S(); delete s; std::string no está destinado a ser utilizado como una clase base.

    – Jens

    15 de diciembre de 2015 a las 11:34

  • portal_id = cake_id funciona, porque con esta construcción CakeID se puede pasar donde se espera una cadena, por ejemplo, al operador de asignación de PortalId. La herencia define una relación “es un”.

    – cdonat

    15 de diciembre de 2015 a las 11:39

  • El problema con la definición de un nuevo tipo con una sintaxis concisa que define implícitamente las operaciones es que no todas las operaciones son relevantes para estos tipos distintos, dependiendo de la intención que solo el programador puede describir en detalle.

    – chico curioso

    8 de enero de 2019 a las 14:26

1646749455 237 Fuertemente tipado usando y typedef
ricardo hodges

Aquí hay una solución completa mínima que hará lo que quieras.

Puede agregar más operadores, etc. para que la clase sea más útil según lo considere oportuno.

#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;
}

aquí hay una versión actualizada que también maneja mapas hash, transmisión a ostream, etc.

Notará que no he proporcionado un operador para convertir a string. Esto es deliberado. Exijo que los usuarios de esta clase expresen explícitamente la intención de usarla como una cadena proporcionando una sobrecarga 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;
}

  • Su solución es excelente, pero agregaría un segundo parámetro de plantilla para el tipo de _value :ideone.com/IVlefk

    – dkg

    15 de diciembre de 2015 a las 11:37

  • @Jendas?? Son 10 líneas de código perfectamente seguro y eficiente que hace exactamente lo que su proyecto necesita. Es el mismo código que usamos en servidores de producción altamente escalables. Pero depende de ti, por supuesto 🙂

    -Richard Hodges

    15 de diciembre de 2015 a las 12:21

  • Y ¡evita el uso de macros!

    – TartánLlama

    15 de diciembre de 2015 a las 12:24

  • Es más escribir que int main() y es… ¿inútil?

    – SS Ana

    27 de junio de 2020 a las 20:44

  • @SSAnne Todo es inútil 🙂 – Para ser honesto, en estos días uso el tipo de retorno final para mantener la coherencia. No lo hice por ninguna razón en particular aparte de la costumbre.

    -Richard Hodges

    29 de junio de 2020 a las 7:45

Las soluciones proporcionadas hasta ahora parecen demasiado complejas, así que aquí está mi intento:

#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>;

  • El problema con este enfoque es que los safe_str de diferentes tipos se derivan públicamente de una cadena. Por lo tanto, son convertibles entre sí. Esto significa que safe_str no es seguro en absoluto.

    -Richard Hodges

    15 de diciembre de 2015 a las 16:39

  • @RichardHodges puedes convertirlos, pero no sé cómo lo harías por accidente.

    – kamikaze

    16 de diciembre de 2015 a las 7:34

  • estás bien. el constructor de cadenas es explícito. Agradable. 🙂

    -Richard Hodges

    16 de diciembre de 2015 a las 9:43

  • @RichardHodges ¿Qué constructor es explícito?

    – chico curioso

    7 de enero de 2019 a las 22:27

  • @RichardHodges En C++ tradicional, solo tiene sentido declarar ctores de un argumento (que no son ctores de copia) como explícitos, porque no hay otro caso en el que una declaración de constructor cree una conversión implícita. (En C ++ moderno, hay un inicio de llave, por lo que es ligeramente diferente).

    – chico curioso

    8 de enero de 2019 a las 14:10

Fuertemente tipado usando y typedef
Jendas

Recientemente me encontré con una biblioteca llamada Tipos con nombre ¡que proporciona azúcar sintáctica bien envuelta para hacer exactamente lo que necesitábamos! Usando la biblioteca, nuestro ejemplo se vería así:

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 biblioteca proporciona muchas más propiedades adicionales como Printable, Incrementable, Hashable, etc. que puede usar para crear, por ejemplo, índices fuertemente tipados para matrices y similares. Consulte el repositorio vinculado para obtener más detalles.

Tenga en cuenta el uso de .emplace(..) método, que es necesario porque el NamedType no es construible por defecto, lo cual es requerido por el []operator.

Fuertemente tipado usando y typedef
TartánLlama

Sería bueno si hubiera una forma estándar de hacer esto, pero actualmente no la hay. Algo podría estandarizarse en el futuro: hay un artículo sobre Definiciones de tipo opaco que intenta hacer esto con alias de función y una construcción de herencia más rica y una en Tipos con nombre que adopta un enfoque mucho más simple con una sola palabra clave nueva para introducir un typedef fuerte, o como quieras llamarlo.

Los suministros de la biblioteca Boost Serialization BOOST_STRONG_TYPEDEF que podría darte lo que quieres.

Aquí hay un reemplazo directo para su SAFE_TYPEDEF que es solo BOOST_STRONG_TYPEDEF sin otras dependencias de refuerzo y modificado ligeramente para que no pueda asignar desde el typedeftipo d. También agregué un constructor de movimiento y un operador de asignación e hice uso de 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; }   \
};

Demo en vivo

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad