Посмотрев конференцию GoingNative 2012 решил попытаться описать «best practice» для написания программ в стиле C++11. Планируется цикл статей, кому интересно,
Зачастую программисты описывают интерфейс, который понятен только им или становится понятным после заглядывания в исходный код метода. Это плохо, если не сказать что ужасно. И дело не только в названии метода.
На первый взгляд вроде не все так плохо, но можете ли вы ответить на вопрос, что за параметр нужно передать в increase_speed? Зависит ли от параметра насколько увеличится скорость? В каких единицах измеряется приращение скорости?
Для ctor-а Rectangle с 4-мя параметрами вообще все сложно. Обозначают ли параметры геометрию прямоугольника? 3-ий параметр это ширина или x-координата второй точки? Ну и т.д.
К тому же при использовании встроенных типов или typedef'ов для встроенных типов мы не сможем написать increase_speed, принимающую параметр в м/с и еще одну версию increase_speed, принимающую км/ч. При использовании встроенных типов это будет одна и та же ф-ция increase_speed(double).
Уже неплохо, но остается проблема с единицами измерения и перегрузкой функций для разных единиц измерений. Давайте условимся все значения выражать в системе СИ и попробуем заставить компилятор проверять единицы измерения на этапе компиляции.
Нам понадобится следующий шаблонный класс, который будет содержать степени при соответствующих основных единицах измерения (метр, килограмм, секунда):
Все значения будем хранить с помощью такого шаблонного класса:
Пример использования:
У нас осталось одно неудобство. Не описаны операторы ��ля нашего класса Value. Т.е. пока мы не можем получить скорость простым делением метров на секунды. Давайте реализуем оператор деления (остальные операторы реализуются по аналогии).
Теперь можем инициализировать значения следующим образом:
Что важно, оверхеда при такой технике проверки единиц быть не должно, все проверки выполняются на этапе компиляции. Для конвертации скорости из км/ч в м/с нужно будет написать функцию, вида:
Код проверялся на gcc 4.6.3.
P.S.: В стандарте предусмотрены User-defined literals (в gcc начиная с версии 4.7), что позволит сократить запись до примерно следующего:
Спасибо за внимание.
Upd:Основной посыл статьи — не лениться писать классы для передаваемых параметров в ваши функции и проверять как можно больше на этапе компиляции.
Зачастую программисты описывают интерфейс, который понятен только им или становится понятным после заглядывания в исходный код метода. Это плохо, если не сказать что ужасно. И дело не только в названии метода.
Пара примеров плохих интерфейсов
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).
Спасибо за внимание.
Upd:Основной посыл статьи — не лениться писать классы для передаваемых параметров в ваши функции и проверять как можно больше на этапе компиляции.