C ++ сильно набрав typedef


49

Я намагався придумати спосіб декларування сильно набраних типів, щоб знайти певний клас помилок на етапі компіляції. Часто буває так, що я набираю 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 я повинен був би сказати компілятору, що мультиплікація швидкості за часом призводить до позиції.

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Јовіћ

Відповіді:


39

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

А якщо говорити про пробіли, то це корисне застосування фантомних типів:

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 і c. - і просто для зручності визначте псевдонім 1км = 1000м.
Джон Перді

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;

1
Це цікаво, але я не впевнений, що він досить сильний. Це забезпечить, що якщо я оголошу функцію з типом typedefed, в якості параметрів можуть використовуватися лише правильні елементи, що добре. Але для кожного іншого використання він додає синтаксичні накладні витрати, не перешкоджаючи змішуванню параметрів. Скажіть такі операції, як порівняння. operator == (int, int) прийме EntityID та ModelID без скарги (навіть якщо явне вимагає, щоб я його передав, це не заважає мені використовувати неправильні змінні).
Кіан

Так. У моєму випадку мені довелося не дозволяти собі присвоювати різні види посвідчень одне одному. Порівняння та арифметичні операції не були моєю основною проблемою. Вищевказана конструкція забороняє призначити, але не інші операції.
mindriot

Якщо ви готові докласти більше енергії для цього, ви можете створити (досить) загальну версію, яка також обробляє операторів, зробивши обгортання класу Explicit найпоширенішими операторами. Див. Приклад на прикладі pastebin.com/FQDuAXdu - вам потрібні досить складні конструкції SFINAE, щоб визначити, чи дійсно клас обгортки надає обернені оператори чи ні (див. Це питання SO ). Зауважте, він все ще не може охопити всі справи і, можливо, не вартує клопоту.
mindriot

Хоча синтаксично елегантне, це рішення спричиняє суттєві штрафи за ефективність для цілих типів. Цілі числа можна передавати через регістри, структури (навіть містять одне ціле число) не можуть.
Ghostrider

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

Примітка. Будь ласка, повідомте мені про будь-які підводні камені / покращення, які ви думаєте.
Ноейн

1
Чи можете ви додати якийсь код, який показує, як використовується цей макрос - як на прикладах в оригінальному запитанні? Якщо так, це чудова відповідь.
Джей Елстон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.