C ++ fortemente tipado typedef

50

Eu tenho tentado pensar em uma maneira de declarar typedefs fortemente tipados, para capturar uma certa classe de bugs no estágio de compilação. Geralmente, digito 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 após uma longa noite de codificação, pode-se cometer erros tolos 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, as sugestões que encontrei para typedefs com tipos fortes incluem o uso de boost, o que pelo menos para mim não é uma possibilidade (eu tenho c ++ 11 pelo menos). Então, depois de pensar um pouco, me deparei com essa idéia e queria executá-la por alguém.

Primeiro, você declara o tipo de base como um modelo. O parâmetro template 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 amigo realmente precisam ser declaradas para frente antes da definição da classe, o que requer uma declaração de encaminhamento da classe de modelo.

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

Finalmente, quando queremos usá-lo, digitamos como:

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

Os tipos agora estão totalmente separados. As funções que usam um EntityID geram um erro de compilador se você tentar alimentar um ModelID, por exemplo. Além de ter que declarar os tipos de 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 ocorreu 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 multiplicar um vetor por um escalar daria outro vetor, para que eu pudesse:

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 o meu typedef fortemente tipado, eu teria que dizer ao compilador que a desmembragem de uma velocidade por tempo 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 explicitamente todas as conversões, o que pode ser um incômodo. Por outro lado, essa limitação pode ajudar a evitar outros tipos de erros (por exemplo, multiplicar uma velocidade por uma distância, talvez, o que não faria sentido nesse domínio). Por isso, estou arrasado e me pergunto se as pessoas têm alguma opinião sobre o meu problema original ou minha abordagem para resolvê-lo.

Kian
fonte
Dê uma olhada nisso: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard
a mesma pergunta está aqui: stackoverflow.com/q/23726038/476681 #
606

Respostas:

40

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

E por falar em espaços, é 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 unidades. 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, estamos usando 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.

Jon Purdy
fonte
2
Hmm, usar o sistema de modelos para impor unidades nas operações é legal. Não tinha pensado nisso, obrigado! Agora, estou pensando se você pode aplicar coisas como conversões entre metros e quilômetros, por exemplo.
Kian
@ Kian: Presumivelmente, você usaria as unidades básicas do SI internamente - m, kg, s, A e etc. - e apenas definiria um apelido 1km = 1000m por conveniência.
Jon Purdy
7

Tive um caso semelhante em que desejava distinguir significados diferentes 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;
};

Obviamente, se você quer ser ainda mais seguro, também pode criar o Tconstrutor explicit. O Meaningé então usado assim:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
mindriot
fonte
11
Isso é interessante, mas não tenho certeza de que seja suficientemente forte. Isso garantirá que, se eu declarar uma função com o tipo typedefed, apenas os elementos certos possam ser usados ​​como parâmetros, o que é bom. Mas para todos os outros usos, ele adiciona uma sobrecarga sintática sem impedir a mistura de parâmetros. Diga operações como comparar. operator == (int, int) aceita um EntityID e um ModelID sem reclamação (mesmo que explícito exija que eu o faça a conversão, isso não me impede de usar as variáveis ​​erradas).
Kian
Sim. No meu caso, tive que me impedir de atribuir diferentes tipos de IDs um ao outro. Comparações e operações aritméticas não eram minha principal preocupação. A construção acima proibirá a atribuição, mas não outras operações.
mindriot
Se você estiver disposto a colocar mais energia nisso, poderá criar uma versão (razoavelmente) genérica que também lide com operadores, fazendo com que a classe Explicit agrupe os operadores mais comuns. Consulte pastebin.com/FQDuAXdu para obter um exemplo - você precisa de algumas construções SFINAE bastante complexas para determinar se a classe wrapper realmente fornece os operadores quebrados ou não (consulte esta pergunta da SO ). Lembre-se, ele ainda não pode cobrir todos os casos e pode não valer a pena.
mindriot
Embora sintaticamente elegante, esta solução sofrerá uma penalidade significativa no desempenho de tipos inteiros. Inteiros podem ser transmitidos através de registradores, estruturas (mesmo contendo um único número inteiro) não.
Ghostrider
1

Não sei ao certo como funciona o seguinte no código de produção (sou iniciante em C ++ / programação, como iniciante em CS101), mas 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; } }
Noein
fonte
Nota: Informe-me de todas as armadilhas / melhorias que você pensa.
Noein
11
Você pode adicionar algum código que mostre como essa macro é usada - como nos exemplos da pergunta original? Se assim for, esta é uma ótima resposta.
Jay Elston