2016-11-02 3 views
1

[изменить: изменено метров/ярдов до foo/bar; речь идет не о преобразовании счетчиков в ярды. ]предпочтительный механизм для прикрепления типа к скаляру?

Каков наилучший способ привязать тип к скаляру, например double? Типичный прецедент - это единицы измерения (но я не ищу фактической реализации, boost has one).

Это, казалось бы, простой как:

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

без добавления вряд ли любой сложности или над головой.

+0

Это просто, но вы должны запрограммировать все юридические преобразования вручную. – StoryTeller

+0

Это кажется хорошим примером для [* пользовательских литералов *] (http://en.cppreference.com/w/cpp/language/user_literal). –

+0

А также для std :: ratio – xvan

ответ

1

Во-первых, да, я думаю, как вы предположили, что вполне разумно, хотя будь то предпочтительнее будет зависеть от контекста. У вашего подхода есть то преимущество, что вы определяете преобразования, которые могут быть не просто простыми умножениями (пример Цельсия и Фаренгейта).

Ваш метод, однако, создает различные типы, что приводит к необходимости создания конверсий, это может быть хорошим или плохим в зависимости от использования.

(Я понимаю, что ваши дворы и счетчики были всего лишь пример, я буду использовать его как просто как пример тоже)

Если я пишу код, который имеет дело с длинами, (большинство их) логика будет одинаковой, независимо от единиц. Хотя я мог бы сделать функцию, содержащую эту логику, шаблоном, чтобы он мог принимать разные единицы, существует еще разумный вариант использования, когда данные нужны из двух разных источников и поставляются в разные единицы. В этой ситуации я предпочел бы иметь дело с одним классом Length, а не с классом на единицу, эти длины могли либо содержать информацию о конверсиях, либо просто использовать один фиксированный блок с преобразованием, выполняемым на этапах ввода/вывода.

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

(Конечно умножение типов отличается.)

-2

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

Стандартное преобразование составляет 1 дюйм 25,4 мм, что означает, что один двор составляет ровно 0,9144 м.

Ни то, ни другое не могут быть представлены точно в бинарной плавающей точке IEEE754.

Если на вашем месте я бы определить

constexpr double METERS_IN_YARDS = 0.9144; 

constexpr double YARDS_IN_METERS = 1.0/0.9144; 

держать ошибок прочь, и работать в двойной точности с плавающей точкой арифметики старинке.

+1

Это не метры или наоборот; это только конкретный пример присоединения типа к «double». –

+0

Для меня это касается неточностей, которые похоронены в чрезмерно разработанном коде. – Bathsheba

+1

Я должен согласиться, он вообще не отвечает на вопрос. В лучшем случае это должен быть комментарий – StoryTeller

5

Я бы выбрал подход с отношением, похожий на std::chrono. (Howard Hinnant показывает, что он в своем недавнем Con C++ 2016 talk about <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>>; 

demo

Это наивная реализация и, вероятно, можно было бы гораздо лучше (по крайней мере, позволяя неявные слепки, где они безопасен), но это доказательство концепции, и это тривиально расширяемо.

Плюсы:

  • meter m = yard{10} либо ошибка во время компиляции или безопасное неявное преобразование,
  • красивые имена типов, вы должны были бы работать против решения очень трудно сделать недопустимое преобразование
  • простой в использовании

Минусы:

  • Возможные целые переполнения/проблемы с точностью (может быть смягчено качеством реализации?)
  • может быть нетривиальной реализовать правильно
+0

Да, я играл с подходом 'std :: chrono'. Ничего плохого в этом нет ... это выглядит несколько сложнее, чем то, я не уверен, где преимущества/компромиссы. Спасибо за указатель на 'std :: ratio'. –

+1

@Dan отредактирован. Я не уверен на 100%, но, обратившись к авторитету, если это достаточно хорошо, чтобы преобразовать наносекунды в эоны, мы должны быть в порядке. – krzaq

+0

Возможно, это зависит от того, насколько вам нужна или нужна безопасность типа? Подход, который я изложил, - это не что иное, как пометка двойника с типом; вы должны перевернуть все остальное «старомодным» способом. Это хорошо? Плохо? Ни? Это зависит? –

Смежные вопросы