Type-rich Programming

в 17:51, , рубрики: best practice, c++, c++11, style guide, Программирование, Совершенный код, метки: , ,

Посмотрев конференцию GoingNative 2012 решил попытаться описать «best practice» для написания программ в стиле C++11. Планируется цикл статей, кому интересно, auto articlesIterator = articles.begin();

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

Пара примеров плохих интерфейсов

void increase_speed(double);
Rectangle(int,int,int,int);

На первый взгляд вроде не все так плохо, но можете ли вы ответить на вопрос, что за параметр нужно передать в increase_speed? Зависит ли от параметра насколько увеличится скорость? В каких единицах измеряется приращение скорости?
Для ctor-а Rectangle с 4-мя параметрами вообще все сложно. Обозначают ли параметры геометрию прямоугольника? 3-ий параметр это ширина или x-координата второй точки? Ну и т.д.
К тому же при использовании встроенных типов или typedef'ов для встроенных типов мы не сможем написать increase_speed, принимающую параметр в м/с и еще одну версию increase_speed, принимающую км/ч. При использовании встроенных типов это будет одна и та же ф-ция increase_speed(double).

Улучшенный вариант

void increase_speed(Speed); // В данной реализации уже видна зависимость от параметра
Rectangle(Point topLeft, BoxWH b); // Понятно, что нужно указывать левую верхнюю точку прямоугольника и ширину с высотой.

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

Сохранение информации о размерности

Нам понадобится следующий шаблонный класс, который будет содержать степени при соответствующих основных единицах измерения (метр, килограмм, секунда):

template<int M, int K, int S>
class Unit { // Система СИ (МКС)
public:
  enum { m = M, kg = K, s = S };
};

Все значения будем хранить с помощью такого шаблонного класса:

template<typename Unit>
struct Value {
  double val; //значение
  explicit Value(double d) : val(d) {}
public:
  /*
   * Ф-ции для определения операторов
   */
  static constexpr int m() {return Unit::m;};
  static constexpr int kg() {return Unit::kg;};
  static constexpr int s() {return Unit::s;};
};
 
typedef Value<Unit<1, 0, -1> > Speed; // Скорость = метр/секунда
typedef Value<Unit<1, 0, -2> > Acceleration; // Ускорение = метр/секунда/секунда
typedef Unit<1, 0, 0> M;
typedef Unit<0, 0, 1> S;

Пример использования:

Acceleration acc1 = Value<Unit<1, 0, -2> >(2); // Ускорение = 2 м/с/с. Ошибки нет.
Acceleration acc2 = Value<M >(2); // Ошибка компиляции.
 
Speed sp1 = Value<Unit<1, 0, -2> >(2); // Ошибка компиляции.
Speed sp2 = Value<Unit<1, 0, -1> >(2); // Скорость = 2 м/с. Ошибки нет.

У нас осталось одно неудобство. Не описаны операторы для нашего класса Value. Т.е. пока мы не можем получить скорость простым делением метров на секунды. Давайте реализуем оператор деления (остальные операторы реализуются по аналогии).

Оператор деления единиц измерения

template<class Value1, class Value2>
auto operator/(Value1 v1, Value2 v2)
  -> Value<Unit<Value1::m() - Value2::m(), Value1::kg() - Value2::kg(), Value1::s() - Value2::s()> > {
    return Value<Unit<Value1::m() - Value2::m(), Value1::kg() - Value2::kg(),
      Value1::s() - Value2::s()> >(v1.val / v2.val);

Теперь можем инициализировать значения следующим образом:

Acceleration acc = Value<M>(100) / Value<S>(10) / Value<S>(1); // Ускорение = 10 м/с/с. Ошибок нет.
Speed sp = Value<M>(100) / Value<S>(20); // Скорость = 5 м/с. Ошибок нет.

Заключение

Что важно, оверхеда при такой технике проверки единиц быть не должно, все проверки выполняются на этапе компиляции. Для конвертации скорости из км/ч в м/с нужно будет написать функцию, вида:

Speed convertSpeed(KmPerHour val);

, где класс KmPerHour — элементарный класс, нужный для организации перегрузки функции convertSpeed. Используйте как можно больше уникальных классов, это поможет использовать перегрузку функций и избавит вас от необходимости использовать разные имена для идеологически одинаковых операций (convertSpeed(KmPerHour) и convertSpeed(KmPerSec) против convertSpeedFromKmPerHour(double) и convertSpeedFromKmPerSec(double)).
Код проверялся на gcc 4.6.3.

P.S.: В стандарте предусмотрены User-defined literals (в gcc начиная с версии 4.7), что позволит сократить запись до примерно следующего:

Speed sp =100m/20s; // При условии переопределения operator"" s(double) и operator"" d(double).

Спасибо за внимание.

Автор: c0d3r


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js