Добавить в корзинуПозвонить
Найти в Дзене

Применяем пользовательские типы для предотвращения ошибок в передаче параметров в С++

Допустим что перед нами стоит задача разработать тип данных, задающий положение на плоскости некторой точки, например, персонажа компьютерной игры. Первый вариант может выглядеть следующим образом: class Coord { int x_, y_; public: Coord(int x, int y) : x_(x), y_(y) {} //далее идут методы для работы с координатами }; Несмотря на крайнюю простоту, при использовании Coord может возникнуть проблема, связанная с тем, что оба параметра конструктора имеют один и тот же тип. Следовательно, передаваемые значения легко перепутать и переставить местами, указав y в качестве первого параметра и x в качестве второго. Как решить данную проблему? Существует несколько вариантов. Вариант 1. Использовать подход применяемый в предметно-ориентированном проектировании (DDD). DDD - это набор принципов и схем, направленных на создание оптимальных систем объектов. Сводится к созданию программных абстракций, которые называются моделями предметных областей. В эти модели входит бизнес-логика, устанавливающая св

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

class Coord {

int x_, y_;

public:

Coord(int x, int y) : x_(x), y_(y) {}

//далее идут методы для работы с координатами

};

Несмотря на крайнюю простоту, при использовании Coord может возникнуть проблема, связанная с тем, что оба параметра конструктора имеют один и тот же тип. Следовательно, передаваемые значения легко перепутать и переставить местами, указав y в качестве первого параметра и x в качестве второго.

Как решить данную проблему? Существует несколько вариантов.

Вариант 1.

Использовать подход применяемый в предметно-ориентированном проектировании (DDD). DDD - это набор принципов и схем, направленных на создание оптимальных систем объектов. Сводится к созданию программных абстракций, которые называются моделями предметных областей. В эти модели входит бизнес-логика, устанавливающая связь между реальными условиями области применения продукта и кодом.

В нашей ситуации наиболее очевидным решением является создание и использование двух программных абстраций (новых типов данных): XCoord и YCoord вместо универсального типа int. Таким образом мы гарантированно избегаем путаницы между x и y, сделав их разными типами при помощи структур-оберток.

struct XCoord {

int value;

constexpr operator int() const { return value; }

};

struct YCoord {

int value;

constexpr operator int() const { return value; }

};

Меняем класс Coord следующим образом:

class Coord {

XCoord x_;

YCoord y_;

public:

Coord(XCoord x, YCoord y) : x_(x), y_(y) {}

//далее идут методы для работы с координатами

};

Теперь компилятор просто не позволит передать неподходящий тип данных в качестве параметра:

Coord pos(XCoord{10}, YCoord{20}); // Правильно

Coord pos(YCoord{20}, XCoord{10}); // Ошибка компиляции!

Coord pos(10, 20); // Ошибка компиляции!

Вариант 2.

Передаем параметры не через конструктор, а через вызовы отдельных методов, например.

class Coord {

int x_{};

int y_{};

public:

Coord& setX(int x) { x_ = x; return *this; }

Coord& setY(int y) { y_ = y; return *this; }

};

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

Использование: auto pos = Coord().setX(10).setY(20);

Обратите внимание на то, что каждый set метод возвращает ссылку на объект. Таким образом, в результате вызова двух методов мы добьемся того же эффекта, что и при использовании конструктора. Однако, код при этом становится более многословным и всё также можно перепутать парметры.

Вариант 3.

Используем назначенные инициализаторы (designated initializers) в C++20.

struct Coord {

int x_;

int y_;

};

Объявляем тип данных и используем имена полей при объявлении: Coord pos{.x_ = 10, .y_ = 20};

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

Вторая проблема - инициализация вида Coord pos{.y = 20, .x = 10};

не пройдет, будет ошибка "error: designator order for field ‘Coord::x_’ does not match declaration order in ‘Coord’" . То есть перечислять переменные в инциализаторе можно только в порядке их объявления в агрегате.

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