Каков наилучший способ присоединения типа к скаляру, такому как double
? Типичный вариант использования — это единицы измерения (но я не ищу фактическую реализацию, Boost имеет один).
Это будет выглядеть так:
template <typename T>
struct Double final
{
typedef T type;
double value;
};
namespace tags
{
struct foo final {};
struct bar final {};
}
constexpr double FOOS_TO_BARS_ = 3.141592654;
inline Double<tags::bar> to_bars(const Double<tags::foo>& foos)
{
return Double<tags::bar> { foos.value * FOOS_TO_BARS_ };
}
static void test(double value)
{
using namespace tags;
const Double<foo> value_in_foos{ value };
const Double<bar> value_in_bars = to_bars(value_in_foos);
}
Это действительно так? Или в этом подходе есть скрытые сложности или другие важные соображения?
Казалось бы, далеко, намного лучше, чем
inline double foos_to_bars(double foos)
{
return foos * FOOS_TO_BARS_;
}
без каких-либо сложностей или накладных расходов.
Во-первых, да, я думаю, то, что вы предложили, вполне разумно, хотя то, будет ли оно предпочтительным, будет зависеть от контекста.
Преимущество вашего подхода состоит в том, что вы определяете преобразования, которые могут быть не просто умножением (например, по Цельсию и по Фаренгейту).
Однако ваш метод создает различные типы, что приводит к необходимости создания конверсий, это может быть хорошим или плохим в зависимости от использования.
(Я ценю, что ваши ярды и метры были только примером, я тоже буду использовать его как пример)
Если я пишу код, который имеет дело с длинами, (большая часть) логика будет одинаковой независимо от единиц измерения. Хотя я мог бы сделать функцию, которая содержит эту логику, шаблоном, который может принимать разные единицы, все же есть разумный вариант использования, когда данные нужны из 2 разных источников и передаются в разные единицы. В этой ситуации я предпочел бы иметь дело с одним классом длины, а не с классом на единицу, эти длины могли бы либо содержать информацию о преобразовании, либо просто использовать одну фиксированную единицу с преобразованием, выполняемым на этапах ввода / вывода.
С другой стороны, когда у нас есть разные типы для разных измерений, например длина, площадь, температура. Не иметь преобразования по умолчанию между этими типами — это хорошо. И хорошо, что я не могу случайно добавить длину к температуре.
(Конечно, умножение типов отличается.)
Я бы пошел с подходом, основанным на соотношении, так же, как std::chrono
, (Говард Хиннант показывает это в своем недавнем C ++ Con 2016 говорить о <chrono>
)
template<typename Ratio = std::ratio<1>, typename T = double>
struct Distance
{
using ratio = Ratio;
T value;
};
template<typename To, typename From>
To distance_cast(From f)
{
using r = std::ratio_divide<typename To::ratio, typename From::ratio>;
return To{ f.value * r::den / r::num };
}
using yard = Distance<std::ratio<10936133,10000000>>;
using meter = Distance<>;
using kilometer = Distance<std::kilo>;
using foot = Distance<std::ratio<3048,10000>>;
Это наивная реализация, и, вероятно, ее можно значительно улучшить (по крайней мере, разрешив неявное приведение там, где они безопасны), но это подтверждение концепции и ее тривиальное расширение.
Плюсы:
meter m = yard{10}
это либо ошибка времени компиляции, либо безопасное неявное преобразование,Минусы:
На мой взгляд, ваш подход чрезмерно разработан до такой степени, что в нем закрались ошибки, которые трудно обнаружить. Даже в этот момент введенная вами синтаксическая сложность позволила вашему обращению стать неточным: вы вышли из восьмого десятичного знака.
Стандартное преобразование составляет 1 дюйм — 25,4 мм, что означает, что один ярд равен 0,9144 метра.
Ни это, ни его обратное не могут быть точно представлены в двоичном формате с плавающей точкой IEEE754.
На вашем месте я бы определил
constexpr double METERS_IN_YARDS = 0.9144;
constexpr double YARDS_IN_METERS = 1.0 / 0.9144;
чтобы избежать ошибок, и работать в старомодной арифметике с плавающей запятой двойной точности.