C ++ строго типизированный typedef

50

Я пытался придумать способ объявления строго типизированных typedefs, чтобы поймать определенный класс ошибок на этапе компиляции. Часто бывает, что я буду вводить int для нескольких типов идентификаторов или вектора для положения или скорости:

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

Это может сделать смысл кода более понятным, но после долгой ночи кодирования можно сделать глупые ошибки, такие как сравнение разных видов идентификаторов или добавление позиции к скорости, возможно.

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

К сожалению, предложения, которые я нашел для строго типизированных typedefs, включают использование boost, что, по крайней мере, для меня невозможно (по крайней мере, у меня есть c ++ 11). Поэтому, немного подумав, я натолкнулся на эту идею и хотел ею воспользоваться.

Сначала вы объявляете базовый тип как шаблон. Однако параметр шаблона ни для чего не используется в определении:

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

Дружественные функции на самом деле должны быть заранее объявлены перед определением класса, что требует предварительного объявления класса шаблона.

Затем мы определяем все члены для базового типа, просто помня, что это класс шаблона.

Наконец, когда мы хотим использовать его, мы определяем его как:

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

Типы теперь совершенно разные. Функции, которые принимают EntityID, будут выдавать ошибку компилятора, если вы, например, попытаетесь передать им ModelID. Помимо необходимости объявлять базовые типы в качестве шаблонов, со всеми вытекающими проблемами, это также довольно компактно.

Я надеялся, что у кого-нибудь были комментарии или критика по поводу этой идеи?

Одна проблема, которая пришла в голову при написании этого, например, в случае положения и скорости, заключалась в том, что я не могу конвертировать между типами так же свободно, как раньше. Где, прежде чем умножить вектор на скаляр, получим другой вектор, так что я мог бы сделать:

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;

С моим строго типизированным typedef мне пришлось бы сказать компилятору, что умножение скорости на Time приводит к позиции.

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

Чтобы решить эту проблему, я думаю, что я должен был бы явно специализировать каждое преобразование, что может быть немного неприятно. С другой стороны, это ограничение может помочь предотвратить другие виды ошибок (скажем, умножение скорости на расстояние, возможно, что не имеет смысла в этой области). Поэтому я разрываюсь и задаюсь вопросом, есть ли у людей какие-либо мнения по поводу моей первоначальной проблемы или моего подхода к ее решению.

Киан
источник
Взгляните на это: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard
тот же вопрос здесь: stackoverflow.com/q/23726038/476681
BЈовић

Ответы:

40

Это параметры фантомного типа , то есть параметры параметризованного типа, которые используются не для их представления, а для разделения разных «пространств» типов с одинаковым представлением.

И если говорить о пробелах, это полезное применение фантомных типов:

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) {  }

Однако, как вы видели, с типами юнитов есть некоторые трудности. Одна вещь, которую вы можете сделать, это разложить единицы в вектор целых показателей по основным компонентам:

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;

Здесь мы используем фантомные значения, чтобы пометить значения времени выполнения с информацией времени компиляции об экспонентах на вовлеченных единицах. Это масштабируется лучше, чем создание отдельных структур для скоростей, расстояний и т. Д., И этого может быть достаточно, чтобы охватить ваш вариант использования.

Джон Перди
источник
2
Хм, использование системы шаблонов для обеспечения юнитов на операциях - это круто. Не думал об этом, спасибо! Теперь я задаюсь вопросом, можете ли вы, например, применять такие меры, как пересчет между метром и километром.
Киан
@Kian: Предположительно, вы будете использовать базовые единицы СИ внутри - m, kg, s, A и т.д. - и просто для удобства определите псевдоним 1km = 1000m.
Джон Перди
7

У меня был похожий случай, когда я хотел различить различные значения некоторых целочисленных значений и запретить неявные преобразования между ними. Я написал общий класс, как это:

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

Конечно, если вы хотите быть еще более безопасным, вы также можете создать Tконструктор explicit. MeaningЗатем используются следующим образом:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
mindriot
источник
1
Это интересно, но я не уверен, что это достаточно сильно. Это гарантирует, что если я объявлю функцию с типом typedefed, в качестве параметров могут использоваться только правильные элементы, что хорошо. Но для любого другого использования это добавляет синтаксические издержки, не предотвращая смешивание параметров. Произнесите такие операции, как сравнение. Оператор == (int, int) примет EntityID и ModelID без жалоб (даже если явное требование, что я приведу это, это не мешает мне использовать неправильные переменные).
Киан
Да. В моем случае я должен был помешать себе присваивать разные идентификаторы друг другу. Сравнения и арифметические операции не были моей главной заботой. Вышеуказанная конструкция запрещает присваивание, но не другие операции.
mindriot
Если вы хотите вложить в это больше энергии, вы можете создать (довольно) универсальную версию, которая также обрабатывает операторы, сделав класс Explicit переносом наиболее распространенных операторов. См. Pastebin.com/FQDuAXdu для примера - вам нужно несколько довольно сложных конструкций SFINAE, чтобы определить, действительно ли класс-оболочка предоставляет обернутые операторы или нет (см. Этот вопрос SO ). Имейте в виду, это все еще не может охватить все случаи и, возможно, не стоит проблем.
mindriot
Хотя это синтаксически элегантно, это решение повлечет за собой значительное снижение производительности для целочисленных типов. Целые числа могут передаваться через регистры, структуры (даже содержащие одно целое число) не могут.
Призрачный гонщик
1

Я не уверен, как это работает в производственном коде (я новичок в C ++ / программировании, например, новичок в CS101), но я сделал это с помощью макросов 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
источник
Примечание: Пожалуйста, дайте мне знать о любых подводных камнях / улучшениях, о которых вы думаете.
Noein
1
Можете ли вы добавить код, показывающий, как используется этот макрос - как в примерах из исходного вопроса? Если это так, это отличный ответ.
Джей Элстон