C ++ strongmente tipado typedef

44

Eu tenho tentado pensar em uma maneira de declarar typedefs strongmente tipados, para pegar uma certa classe de bugs no estágio de compilação. Geralmente, é o caso que eu digitei um int em vários tipos de ids, ou um vetor para posição ou velocidade:

typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;

Isso pode tornar a intenção do código mais clara, mas depois de uma longa noite de codificação, pode-se cometer erros bobos, como comparar diferentes tipos de ids ou adicionar uma posição a uma velocidade, talvez.

EntityID eID;
ModelID mID;

if ( eID == mID ) // <- Compiler sees nothing wrong
{ /*bug*/ }


Position p;
Velocity v;

Position newP = p + v; // bug, meant p + v*s but compiler sees nothing wrong

Infelizmente, sugestões que eu encontrei para typedefs strongmente tipados incluem usar boost, que pelo menos para mim não é uma possibilidade (eu tenho pelo menos c ++ 11). Então, depois de pensar um pouco, descobri essa ideia e quis que ela fosse executada por alguém.

Primeiro, você declara o tipo base como um modelo. O parâmetro do modelo não é usado para nada na definição, no entanto:

template < typename T >
class IDType
{
    unsigned int m_id;

    public:
        IDType( unsigned int const& i_id ): m_id {i_id} {};
        friend bool operator==<T>( IDType<T> const& i_lhs, IDType<T> const& i_rhs );
};

As funções de amigos precisam ser encaminhadas antes da definição da classe, o que exige uma declaração de encaminhamento da classe de modelo.

Em seguida, definimos todos os membros para o tipo base, lembrando que é uma classe de modelo.

Por fim, quando queremos usá-lo, digitamos como:

class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;

Os tipos agora são totalmente separados. As funções que usam um EntityID lançarão um erro do compilador se você tentar alimentá-las com um ModelID, por exemplo. Além de ter que declarar os tipos base como modelos, com os problemas que isso implica, também é bastante compacto.

Eu esperava que alguém tivesse comentários ou críticas sobre essa ideia?

Uma questão que me veio à mente ao escrever isso, no caso de posições e velocidades, por exemplo, seria que não posso converter entre tipos tão livremente quanto antes. Onde antes de multiplicar um vetor por um escalar daria outro vetor, então eu poderia fazer:

typedef float Time;
typedef Vector3 Position;
typedef Vector3 Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t;

Com meu typedef strongmente digitado, eu teria que dizer ao compilador que o multiregamento de Velocity by a Time resulta em uma posição.

class TimeT;
typedef Float<TimeT> Time;
class PositionT;
typedef Vector3<PositionT> Position;
class VelocityT;
typedef Vector3<VelocityT> Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t; // Compiler error

Para resolver isso, acho que teria que especializar cada conversão explicitamente, o que pode ser um tipo de incômodo. Por outro lado, essa limitação pode ajudar a evitar outros tipos de erros (digamos, multiplicar uma Velocidade por uma Distância, talvez, o que não faria sentido nesse domínio). Então estou dividido e imaginando se as pessoas têm alguma opinião sobre meu problema original ou sobre minha abordagem para resolvê-lo.

    
por Kian 05.06.2014 / 23:26
fonte

3 respostas

34

Estes são parâmetros de tipo fantasma , isto é, parâmetros de um tipo parametrizado que são usados não para sua representação, mas para separar diferentes “espaços” de tipos com a mesma representação.

E falando em espaços, essa é uma aplicação útil de tipos fantasmas:

template<typename Space>
struct Point { double x, y; };

struct WorldSpace;
struct ScreenSpace;

// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) { … }

Como você viu, existem algumas dificuldades com os tipos de unidade. Uma coisa que você pode fazer é decompor unidades em um vetor de expoentes inteiros nos componentes fundamentais:

template<typename T, int Meters, int Seconds>
struct Unit {
  Unit(const T& value) : value(value) {}
  T value;
};

template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
  return a.value / b.value;
}

Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);

// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;

Aqui, usamos valores fantasmas para marcar valores de tempo de execução com informações em tempo de compilação sobre os expoentes nas unidades envolvidas. Isso é melhor do que criar estruturas separadas para velocidades, distâncias e assim por diante, e pode ser suficiente para cobrir seu caso de uso.

    
por 06.06.2014 / 01:51
fonte
6

Eu tive um caso semelhante em que queria distinguir diferentes significados de alguns valores inteiros e proibir conversões implícitas entre eles. Eu escrevi uma classe genérica como esta:

template <typename T, typename Meaning>
struct Explicit
{
  //! Default constructor does not initialize the value.
  Explicit()
  { }

  //! Construction from a fundamental value.
  Explicit(T value)
    : value(value)
  { }

  //! Implicit conversion back to the fundamental data type.
  inline operator T () const { return value; }

  //! The actual fundamental value.
  T value;
};

É claro que, se você quiser ser ainda mais seguro, também poderá tornar o T construtor explicit . O Meaning é então usado assim:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
    
por 06.06.2014 / 14:04
fonte
1

Não sei como funciona o seguinte no código de produção (sou iniciante no C ++ / programming, como, CS101 beginner), mas eu preparei isso usando o macro sys do C ++.

#define newtype(type_, type_alias) struct type_alias { \

/* make a new struct type with one value field
of a specified type (could be another struct with appropriate '=' operator*/

    type_ inner_public_field_thing; \  // the masked_value
    \
    explicit type_alias( type_ new_value ) { \  // the casting through a constructor
    // not sure how this'll work when casting non-const values
    // (like 'type_alias(variable)' as opposed to 'type_alias(bare_value)'
        inner_public_field_thing = new_value; } }
    
por 20.05.2015 / 10:05
fonte