LINUX.ORG.RU

Типы для физических величин на C++: поругайте

 , ,


3

4

Начал пилить некую систему типов для физических величин, где значение величины имеет семантику умножения безразмерного счётчика на абстрактную единицу измерения, чтобы не надо было каждый раз в публичных API вида SetFrequency(int freq) выяснять что же этот int хранит, а также чтобы не давало складывать метры с литрами и записывать результат в секунды (а также метры с километрами без должной конвертации первого или второго). Существующих велосипедов не нашёл, кроме разве Boost.Units, но это страшный overkill, надо чтобы было маленькое и в одном заголовке.

Базовая идея проста и описана в книжке Страуструпа в главе про <chrono> --

template<typename Rep, typename Period = std::ratio<1>>
class X
{
    Rep mCount;
};
 -- сохраняем значение безразмерного счётчика в фундаментальном типе Rep (int, double, etc), а десятичную приставку в виде рациональной дроби держим только в системе типов на этапе компиляции.

Код тут: https://github.com/Jajauma/SIUnits, содрано с std::chrono::duration, остатки libstdc++ ещё не вычистил полностью, так что на MSVC видимо работать не будет (а может и нигде не будет), главый шаблон SI::Units, для демонстрации там же определены типы Frequency и Length и нескучные пользовательские литералы типа _km, _mm и т.д.

★★★★★

#ifndef HEADER_700EA8EA_5F41_11E7_93E4_74D4359F3068_INCLUDED

Эээ, что думаешь о pragma once?

А так круто, влепил звезду.

BruteForce ★★★ ()

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

morse ★★★★★ ()
Последнее исправление: morse (всего исправлений: 1)
Ответ на: комментарий от BruteForce

Я не знаю, я слышал что pragma это UB и gcc ханойские башенки может запускать. У меня всё равно сниппет в виме эти фигни генерит, ну можно и pragma сгенерить.

d_a ★★★★★ ()
Ответ на: комментарий от morse

С точки зрения по памяти стоит ровно сколько же, сколько стоит Rep[resentation] (т.е. это concrete type, в терминологии Страуструпа). С точки зрения быстродействия ... на смешанной арифметике, скажем, с килогерцами и мегагерцами, конечно будут умножения и деления для конвертации (как в UnitsCast написано). Ну, на Count() ещё накладные расходы, но её можно выпилить и заменить на иммутабельное поле const Rep Count. Главное чтобы с фундаментальными типами не смешивалось, для этого нужен Count() или Count, т.е. чтобы явно вытаскивали _безразмерное_ число.

d_a ★★★★★ ()
Последнее исправление: d_a (всего исправлений: 1)
Ответ на: комментарий от d_a

И тут соглашусь.

Дело скорее в том, что те, кто разрабатывают софт где оно может понадобится

а) работают в НИИ, получают нищенскую ЗП и им плевать как это поддерживать.

б) на аутсорсе, со всеми вытекающими.

BruteForce ★★★ ()
Ответ на: комментарий от BruteForce

Я не в нии и зп не совсем нищенская, но мне надо. А то достало в половине программы int значит килогерцы, а в другой половине герцы. Ладно если хоть документация есть, но она в комментариях в лучшем случае, а компилятор комментарии не читает, не сегодня завтра отвалится.

d_a ★★★★★ ()
Ответ на: комментарий от morse

У нас у самих была: генератор выдает частицы с энергией в ГэВах, а симулятор ждет МэВы. Но все это решилось банальным домножением, и никто больше этим не парился.

Главное падабум нам всем не устройте :)

d_a ★★★★★ ()
Ответ на: комментарий от morse

В качестве развлечения сойдет, но в реале вряд ли кому-нибудь понадобится. В научном софте важна скорость, поэтому никто не будет заморачиваться оберткой примитивов в многоэтажную абстракцию

Что-то подобное есть в OpenFOAM и вполне себе используется, на скорость не сильно влияет

Sahas ★★★★★ ()

чтобы не надо было каждый раз в публичных API вида SetFrequency(int freq) выяснять что же этот int хранит, а также чтобы не давало складывать метры с литрами и записывать результат в секунды

Эммм...
А почему нельзя сделать классы/литералы для единиц измерений, и преобразований для всяких кило-, мега-, гига-? Или я не уловил проблематики?

#include <iostream>
using namespace std;

class Frequency
{
public:
    void Print() const { cout << hertz << "Hz\n"; }

    explicit constexpr Frequency(unsigned int h) : hertz(h) {}
private:
    unsigned int hertz;
};
constexpr Frequency operator"" _Hz(unsigned long long hz)
{
    return Frequency{hz};
}
constexpr Frequency operator"" _kHz(long double khz)
{
    return Frequency{khz * 1000};
}

int main()
{
    Frequency(44100_Hz).Print();
    Frequency(44.1_kHz).Print();
    return 0;
}
(C) https://stackoverflow.com/questions/21868368/units-of-measurement-in-c

Kroz ★★★★★ ()
Ответ на: комментарий от Kroz

Или я не уловил проблематики?

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

d_a ★★★★★ ()
Ответ на: комментарий от d_a

Поэтому ты молодец и получил от меня звезду на гитхабе =)

BruteForce ★★★ ()
Ответ на: комментарий от BruteForce

Это всё сделали в <chrono> в Cxx11, на самом деле :) Но только для единиц времени, очевидно. Мне кажется несправедливо, чем пространство так отличается от времени, что не заслужило такого внимательного обращения?

d_a ★★★★★ ()
Ответ на: комментарий от d_a

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

Я не предлагал именно в целочисленную величину, пример просто скопипастил с stackoverflow. То, что я предлагаю - в одну величину. long double имеет диапазон ±1.7E±308 - тебе не хватит?

Комплексные числа - С++ из коробки с ними работает. Сам не пробовал, но не думаю что там что-то сложное.

Так чего не хватает?

Kroz ★★★★★ ()
Ответ на: комментарий от BruteForce

Чувак и сделал классы/литералы.

Если я правильно всё понял, он отдельно хранит значение, отдельно кило/мега/гига. То, что я предлагаю,- избавиться от кило/мега/гига, а при присваивании сразу умножать на мультипликатор.

То есть вместо этого:

constexpr Frequency<Integer, std::kilo> operator"" _kHz(Integer count)
{
    return Frequency<Integer, std::kilo>{count};
}

Вот это:
constexpr Frequency operator"" _kHz(long double khz)
{
    return Frequency{khz * 1000};
}

Kroz ★★★★★ ()

Хм, странно, что они решили передавать эти типы по константной ссылке в libstdc++. Они же по размеру примитивов, в большинстве случаев одного. Заметной разницы быть не должно из-за оптимизаций, но всё же.

xaizek ★★★★★ ()
Ответ на: комментарий от Kroz

То, что я предлагаю - в одну величину. long double имеет диапазон ±1.7E±308 - тебе не хватит?

Это очень неэффективно по памяти, всё равно что в качестве кодировки по умолчанию предложить UTF32. Кроме того, если сохранять мантиссу в целочисленном представлении, во многих преобразованиях можно использовать целочисленную lossless арифметику на сложениях и умножениях. Пример -- чтобы точно сохранить 125ГГц, мне нужен всего 1 char:

Frequency<char, std::giga>{125}
, а не 16, как long double.

d_a ★★★★★ ()
Ответ на: комментарий от Kroz

Если я правильно всё понял, он отдельно хранит значение, отдельно кило/мега/гига

Приставка с десятичной экспонентой сохраняется только на стадии компиляции, как часть типа, её нет в рантайме.

d_a ★★★★★ ()

Годно, получай звезду.

А... почему у тебя пробелы, а не indent with tabs, align with spaces? Судя по коду, ты задумывался насчёт стиля ведь.

Алсо, почему

    template <typename Rep2 = Rep>
    typename std::enable_if<!std::is_floating_point<Rep2>::value, Units&>::type
    operator%=(const Rep& rhs)
    {
        mCount %= rhs;
        return *this;
    }

, а не

    template <typename Rep2 = Rep,
              typename = std::enable_if<!std::is_floating_point<Rep2>::value>::type>
    Units& operator%=(const Rep& rhs)
    {
        mCount %= rhs;
        return *this;
    }
?

И зачем в обоих operator%= Rep2?

intelfx ★★★★★ ()
Последнее исправление: intelfx (всего исправлений: 3)

Годно, но проведите чистку кода и иерархию проекта.

andreyu ★★★★★ ()
Ответ на: комментарий от morse

Вероятность того что кто-то нечаянно подсунет метры вместо секунд так и так нулевая.

Святая наивность. Я видел, как давление в метрах измеряли внутри кода.

peregrine ★★★★★ ()
Ответ на: комментарий от intelfx

И где в билдсистеме таргет из хедеров, его экспорт и т. п. modern CMake?

intelfx ★★★★★ ()
Ответ на: комментарий от d_a

Пример — чтобы точно сохранить 125ГГц, мне нужен всего 1 char:

Можно я чуток покритикую?

Давай чуть шире посмотрим на проблему.

Какие TOP3 задачи твоей библиотеки? Как я понял: 1) поддержка всех единиц измерения 2) учет кило/мега/гига/... 3) работа с комплексными числами. Я не увидел в твоем коде Джоулей и Люменов. У тебя программа уже падает на менее 8Гб ОЗУ? Если нет, тогда какого рожна ты сейчас печешься об экономии памяти?

Я на своем опыте вывел основное правило: сначала добейся чтобы программа выполняла основную задачу, после этого оптимизируй. Попытка писать сразу оптимальную программу приводит провалу проекта (заказчику, или, если just for fun, ты его просто его забрасываешь), и, что обидно, не уберегает от дальнейших оптимизаций/реинжиниринга.

Самое интересное, что я изобрел велосипед, так как это, и много больше, уже давно оказалось изобретено до меня.

Чуть позже я еще сформулировал следующее: любая серьезная программа должна пройти несколько циклов рееинжиниринга. Поэтому единственная оптимизация в процессе написания - простота дальнейшего реинжиниринга. Это подразумевает комментарии, читабельный код, (по ссылке) «правило модульности», «правило расширяемости», некоторые другие приёмы. Если ты в процессе написания видишь место где можно оптимизировать, то ты себя сдерживаешь (ведь руки так и чешутся, я знаю), пишешь коммент TODO, и дальше фокусируешься на основной задаче.

Теперь от философии к конкретике («правила» - из всё той же библии):
1) Я тебе рекомендую забыть об экономии памяти; с этой точки зрения моя идея вроде как лучше.
2) Если только у тебя нет задачи точности вычислений до 10-го знака после запятой (а в TOP 3 ее нет) - это тоже оптимизация - ... до лучших времен. Единственное что, чтобы дальше было легче это имплементировать (и чтобы следовать «правилу расширяемости»), ты мог бы сделать typedef для базового типа значения чтобы потом легче было его изменять (а я для таких целей иногла использую class или enum - потому что см. вопрос в топике и ответ тут)
3) Я правильно понимаю, что у тебя куски кода с литералами для разных единиц измерений и разных префиксов будут ну очень похожи друг на друга? Тогда рекомендую обратить внимание на «Правило генерации».

Удачи.

Kroz ★★★★★ ()
Ответ на: комментарий от intelfx

Второй подход не работает для овелоадов. Он только как альтернатива (причем не лучшая) к static_assert.

KennyMinigun ★★★★★ ()
Ответ на: комментарий от KennyMinigun

Так будет работать и с перегрузкой:

    template <typename Rep2 = Rep,
              std::enable_if<!std::is_floating_point<Rep2>::value, int>::type = 0>
    Units& operator%=(const Rep& rhs)
    {
        mCount %= rhs;
        return *this;
    }

xaizek ★★★★★ ()
Ответ на: комментарий от intelfx

А... почему у тебя пробелы, а не indent with tabs, align with spaces? Судя по коду, ты задумывался насчёт стиля ведь.

Форматирируется автоматически, кнопочкой в виме, одним и тем же .clang-format на котором остановились на работе, а табы у нас не в почёте.

Касательно operator%= -- в std::chrono::duration так было сделано, я тоже очень долго втыкал и думал что ошибка, решил что может здесь кто-нибудь объяснит, впрочем, ниже выше уже подсказывают ^_~

d_a ★★★★★ ()
Последнее исправление: d_a (всего исправлений: 1)
Ответ на: комментарий от intelfx

И где в билдсистеме таргет из хедеров, его экспорт и т. п. modern CMake?

Так а нету билдсистемы, всё в одном hpp. Просто чтобы выкладывать я решил тестики написать, вместо документации, вот cmake какие-то простые тестики и собирает.

d_a ★★★★★ ()
Ответ на: комментарий от d_a

Касательно operator%= — в std::chrono::duration так было сделано, я тоже очень долго втыкал и думал что ошибка

С Rep2 это не для перегрузки, а чтобы компилировалось для типов, которые вылят std::enable_if. Если убрать Rep2, то компилятор не даст создать класс с float, например. А c Rep2 метод проверяется на адекватность только во время вызова, а не при использовании класса, так как компилятору надо откладывать проверку чтобы знать тип Rep2 наверняка.

xaizek ★★★★★ ()
Ответ на: комментарий от KennyMinigun

Второй подход не работает для овелоадов.

Гм, почему?

intelfx ★★★★★ ()
Ответ на: комментарий от xaizek

В чём принципиальная разница между шаблонным параметром типа или шаблонным целочисленным параметром? Ткни в доку, пожалуйста.

intelfx ★★★★★ ()
Ответ на: комментарий от xaizek

Всё равно не понятно, зачем там Rep2, если он нигде вообще в коде метода не встречается кроме самого объявления?

intelfx ★★★★★ ()
Последнее исправление: intelfx (всего исправлений: 1)
Ответ на: комментарий от intelfx

Он не только объявляется, но ещё и используется вместо Rep.

xaizek ★★★★★ ()
Ответ на: комментарий от intelfx

А вот фиг его знает, где оно в стандарте, не нахожу. Но идея в том, что значения по умолчанию не участвуют в исключении функции из списка перегрузок, а невозможность вывести тип значения не-шаблонного шаблонного параметра — исключает, так как вывод типов аргументов становится невозможным. Оно скорее всего разнесено в стандарте на две или три части, надо искать.

xaizek ★★★★★ ()
Ответ на: комментарий от intelfx

Гм, почему?

Default template arguments are not part of the signature of a template

https://stackoverflow.com/questions/11055923/stdenable-if-parameter-vs-templa...

// как написал xaizek — в стандарте отыскать довольно сложно лень. Думаю в этом случае можно просто поверить на слово

KennyMinigun ★★★★★ ()
Последнее исправление: KennyMinigun (всего исправлений: 1)

template<typename Rep, typename Period = std::ratio<1>>

До чего дошёл прогресс... Когда я занимался обсчётами лаб на Си лет 25 назад, то все либы работали строго в Си (это к вопросу «SetFrequency(int freq) выяснять что же этот int хранит»), а величины я делал тупо на дефайнах.

#define mmHg *133.3223

double pressure = 760 mmHg;

KRoN73 ★★★★★ ()
Последнее исправление: KRoN73 (всего исправлений: 1)
Ответ на: комментарий от KRoN73

А 50 лет назад это делали бы на ассемблере для PDP. А уж 100 ... :)

d_a ★★★★★ ()
Ответ на: комментарий от d_a

Нет, на ассемблерах 50 лет назад так было не сделать. Из тогдашних языков, наверное, только на Форте так можно.

KRoN73 ★★★★★ ()
Ответ на: комментарий от KRoN73

Так не сделать это как? На ассемблере нельзя умножить на 133? Не верю. Потому что больше ваш пример не содержит _ничего_. double по прежнему безразмерен, а семантика pressure (что это 760*133.3223*10^0 Па) записана где-то в голове программиста или в комментарии, в лучшем случае.

d_a ★★★★★ ()
Ответ на: комментарий от d_a

Так не сделать это как? На ассемблере нельзя умножить на 133?

На старых ассемблерах не реализовать такой семантики. При чём тут умножение.

а семантика pressure (что это 760*133.3223*10^0 Па) записана где-то в голове программиста

Если голова есть — этого хватит. Если головы нет — не спасёт никакая семантика.

KRoN73 ★★★★★ ()

https://github.com/Jajauma/SIUnits/blob/6907d0f22be339b0c84bdc22f60d7630ef56ee5e/SIUnits.hpp#L463-L555

Вот эта портянка, от неё в глазах рябит. И тесты LengthTests.cpp и FrequencyTests.cpp туда же. Я бы заменил на макросы:

SI_UNIT(Frequency, Hertz)

SI_UNIT_LIT(Frequency, kilo,     _kHz)
SI_UNIT_LIT(Frequency, mega,     _MHz)
SI_UNIT_LIT(Frequency, giga,     _GHz)
SI_UNIT_LIT(Frequency, tera,     _THz)
SI_UNIT_LIT(Frequency, ratio<1>, _Hz)

SI_UNIT(Length, Meter)

SI_UNIT_LIT(Length, nano,     _nm)
SI_UNIT_LIT(Length, micro,    _mcm)
SI_UNIT_LIT(Length, milli,    _mm)
SI_UNIT_LIT(Length, centi,    _cm)
SI_UNIT_LIT(Length, ratio<1>, _m)
SI_UNIT_LIT(Length, kilo,     _km)

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

NeXTSTEP ★★ ()
Последнее исправление: NeXTSTEP (всего исправлений: 1)

Почему _mcm, а не _um? Микрометры обозначаются μm, но раз уж _μm не подходит, то:

In circumstances, in which only the Latin alphabet is available, ISO 2955 (1974,[6] 1983[7]), DIN 66030 (Vornorm 1973;[8] 1980,[9][10] 2002[11]) and BS 6430 (1983) allow the prefix μ to be substituted by the letter u

https://en.wikipedia.org/wiki/Micro-

NeXTSTEP ★★ ()

Идея: производные величины. Например, умножение величины в метрах на такую же даст площадь в квадратных метрах. Ньютоны на метры — джоули и т.п.

NeXTSTEP ★★ ()

Boost.Unit, proposal with std::unit. Тред не читал

zamazan4ik ★★ ()
Ответ на: комментарий от KRoN73

Если голова есть — этого хватит. Если головы нет — не спасёт никакая семантика.

Если бы это было правда, все использовали бы void* вместо типов и не ломали копья в спорах о языках и техниках. Но это не имеет никакого отношения к реальности, в которой нужно управлять процессом, а не ждать милостей от человеческой природы, поэтому начиная с C++11 единицы для времени (duration и на его основе time_point) сделаны с использованием системы типов C++ именно как написано в OP.

d_a ★★★★★ ()
Ответ на: комментарий от zamazan4ik

Boost.Unit

OP видимо тоже не читал.

proposal with std::unit

Мне сейчас надо (а не через 10 лет).

d_a ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.