LoginSignup
29
13

More than 3 years have passed since last update.

Mitama.Dimensionalを支える技術

Last updated at Posted at 2019-05-20

ファイル 2019-05-12 3 16 02.jpeg

LoliGothick/mitama-dimensional(GitHub)

モチベーション

Mitama.Dimensional(以下、本ライブラリ)はC++17を使って書かれた、次元解析で型安全に単位つきの計算ができるライブラリ。
設計はBoost.Unitsがベースになっている。
既存の単位付き計算ライブラリでユーザー定義の次元を扱おうとしてゲロを吐いたためライブラリをこねこねした。
「C++ Units」で検索して出てくるやつがbuilt on c++14って書いてあるけど魔黒だらけでいじたくないし、Boost.Unitsも古くていじりたくないし。
もうC++17で書くかとういう流れで書いた。
できるだけ簡単にユーザーが次元を増やせるライブラリがほしかった。

キログラム原器が引退し、新しいSI単位の定義が施行されることを記念して2019/5/20にリリースしようと開発してきた。

単位つけたい人はぜひ試してみてください。
ドキュメントを頑張って書きましたが、めんどくさいので英語しか書いてません。

既存のライブラリと比べて何がすごい?

単位ではなく、新しい次元そのものを作ることができるという特徴がある。

既存の単位つき計算ができるライブラリには

などがある。

これらのライブラリでは物理でつかう

  • Length(長さ)
  • Time(時間)
  • Mass(質量)
  • Electric Current(電流)
  • Thermodynamic Temperature(熱力学温度)
  • Amount of Substance(物質量、モル)
  • Luminous Intensity(光度)

という7つの次元(+平面角度、立体角度、バイトなどのいくつかの無次元)しか扱えないという事実がある。
Boost.Unitsはできそうだけど、ものすごくめんどくさそうで調べるの断念した。
(多分できたとしても、ものすごくめんどくさそうだし、内部いじらないといけなくてダーティーハックになるかも)。

本ライブラリでは次元を自由に作ることができ、統合された方法で簡単にそれらを扱うことができる。

例えば、通貨単位が扱いたくなったとき、既存のライブラリで対応するためにはライブラリに手を加える必要があり非常に困難であるが、本ライブラリでは用意された方法でユーザーが型を作れば簡単にできる。

そもそもnholthaus/unitsは魔黒が横行していてなんか微妙。
魔黒には魔黒の良さがあるんだろう、知らんけど。

既存のライブラリと比べて劣っているところ

  • CGS単位系のサポートがない(するつもりがない)
  • cmath にある数学関数のサポートが足りてない(そのうちする)
  • C++17より古い言語仕様を一切サポートしない(強い意志)

安定のBoost.Unitsでは当然のようにCGSのサポートがある。
余力があったらライブラリ側でも実装しようかなと思っている。

ライブラリ機能一覧

  • 次元量を表すクラス、quantity_t
  • 次元量の算術演算
  • 次元量の自動単位変換
  • 次元量のための数学関数
  • 定義済みの次元たくさん(120個以上、SIだけで比べてもBoost.Unitsよりは確実に少ない)
  • ユーザー定義の次元型を扱える
  • フォーマットつき出力
  • 次元篩型
  • コンパイル時次元量

Mitama.Dimensionalを支える技術

C++でこのようなライブラリを実現できた背景とその技術について解説する。
以下では普通の(実行時の)プログラミングについての解説は一切おこわれないため注意すること。

Phantom Type(幽霊型)

型パラメータの宣言には現れるが、定義には使用されないタグ型を使う技法。

例えば、以下のようにクラス内でTag型の変数を持つわけではないがTagによって、クラスを区別するため型を持つのは幽霊型の技法である。

template < class T, class Tag >
struct tagged_t{
    T x_;
};

タグディスパッチによる実装の選択も幽霊型にあたる。

Tという型にDimという次元をくっつけた型を考える場合、Dimを幽霊型にすればうまくいくことにお気づきになられたであろう。

template < class Dim, class T >
struct quantity_t {
    T value_;
};

これで、大丈夫。
このアイデアはこの種のライブラリの基幹であり、どの言語でもだいだいこの技法を使っていると思われる。
問題はDimの部分をどうするかである。

可変長テンプレート

C++の強力な機能の一つに可変長テンプレートがある。
ジェネリクス様機能を持つプログラミング言語はたくさんあれど、可変長のジェネリクスを持つ言語は殆ど無い。

「C++ dimensional analysis」とかで検索するとだいたいこのようなものがヒットしがちである。

実装はだいたい以下のようになっている。

template<class T, // Precision
    int m, // Mass
    int l, // Length
    int t, // Time
    int q, // Charge
    int k, // Temperature
    int i, // Luminous Intensity
    int a> // Angle

これも幽霊型である。
SIの7つの次元の指数を幽霊型として使っている(型じゃないけど)。

本ライブラリでは扱える次元を固定しないことを目標としたため、この考えをやめる必要があった。
さいわいにもC++には可変長テンプレートという機能があり、次元と指数のペアになった型のパラメータパックで次元を表現することは容易であった。
簡略化して手順を示す。

まず、次元を表す型を用意する。

struct length {
    using is_base_dimension = void;
};

次に、次元と指数のペアの型を作る。

template < class, std::intmax_t, class >
struct unit_t;

template < class T, std::intmax_t N >
struct unit_t<T, N, typename T::is_base_dimension> {
    using dimension_type = T;
};

次に、unit_tのシーケンスを格納する型を作る。

template <class T, class... Types>
inline constexpr std::size_t dimension_count_v =
    (static_cast<std::size_t>(
         std::is_same_v<
             typename T::dimension_type,
             typename Types::dimension_type>
    ) + ... + std::size_t{});

template <class... Units>
struct dimensional_t
{
  static_assert(std::conjunction_v<std::bool_constant<
                    (dimension_count_v<Units, Units...> == 1)>...>,
                "Error: specified units contains same dimension"); // sanity check
};

このdimensional_tを幽霊型として使う。

template < class, class >
class quantity_t;

template < class T, class... Units >
class quantity_t<dimensional_t<Units...>, T> {
    T value_;
// ...
};

めでたしめでたし。
いや、全然めでたくない。

先程の素朴な実装と違ってインデックスがついていないので順番が違うだけで違う型になってしまう!

Meta Programming

dimensional_tは可変長テンプレートである。
可変長テンプレートは型上のstd::vectorみたいなものである。
unit_tは次元と指数のペアであるので、まんまstd::pair<Key, Value>である。
dimensional_tはペアが格納されたvectorのようなものであるということだ。
std::vector<std::pair<Key, Value>>を扱うのと大差ない。

皆さんは2重ループを回す関数を書くことはできるだろうか?僕にはできる。

実行時にできることはコンパイル時にもできる。僕にはできる。

メタプログラミングの時間だ。

TypeList(型リスト)

まず、可変長テンプレートを扱うために、実行時のコンテナ操作に相当するメタ関数を量産する。

量産と言っても、必要なものしか書いてないので160行しかない。

可変長テンプレートを型のコンテナのように扱う技術はメタプログラミングの要素技術の一つである(?)。

どうやって次元の同一性を確かめるのか

  • 一方のdimensional_tに含まれる次元が他方にも含まれ
  • 指数が一致する

ということを確かめればよい。

一見二重ループが必要に思えるが、パラメータの数が異なる場合を枝刈りをすることで一回のループですむ。
準備としてdimensional_t<Units...>Units...をすべてprivate継承しておく。

template <class... Units>
struct dimensional_t: private Units... // タグとして継承する

そうしておいて、つぎのように一方のdimensional_tのパラメータを他方がすべて継承しているかを確かめればよい。

template < class, class > struct is_same_dimensional_impl: std::false_type {};

template <class... Units1,
          class... Units2>
struct is_same_dimensional_impl<dimensional_t<Units1...>,
                                dimensional_t<Units2...>>
    : std::conjunction<
        std::bool_constant<sizeof...(Units1) == sizeof...(Units2)>,
        std::is_base_of<typename Units1,
                        dimensional_t<Units2...>>...> {};

無駄にテンプレートを実体化させないためにstd::conjunctionを使うことが肝要である。

どうやって掛け算とわり算をしたあとの次元を求めるのか

素朴な実装に再登場してもらおう。

template<class T, // Precision
    int m, // Mass
    int l, // Length
    int t, // Time
    int q, // Charge
    int k, // Temperature
    int i, // Luminous Intensity
    int a> // Angle

この実装では掛け算をしたあとの次元を求めるのは簡単である。
単にそれぞれの同じ次元の指数を足せばいいのである。

可変長テンプレートの場合は……
どうしようもないので、ゴリ押しメタプログラミングするしかなさそう。

空のdimensional_tを用意して、

  • 同じ次元があれば足し算(引き算)して入れる
  • そうでなければそのまま入れる
  • 引き算した結果指数がゼロになったら入れない

みたいなことをクラスの再帰で必死にやる。

必死にやった結果がこちら(閲覧注意)。

実装が汚いので書き直したいけど、リファクタリングする気が起きない。

依存型

C++はテンプレートに値を埋め込むことができるtemplate non-type parameterという機能がある。
テンプレートに値を埋め込むことで無理やり依存型のようなことができる。

本ライブラリではこの機能を使ってrefinement type(篩型)を作ってみた。

Refinement Type

使い方の概要を以下に示す。

#include <dimensional/quantity.hpp>
#include <dimensional/static_quantity.hpp>
#include <dimensional/systems/si/meter.hpp>
#include <dimensional/io.hpp>
#include <iostream>

// predicate examples:
template < auto Arg >
struct is_even: std::bool_constant<Arg % 2 == 0> {};

int main(){
    using mitama::systems::si::meter_t;
    using mitama::static_quantity, mitama::refined, mitama::quantity_t;
    using namespace mitama::literals::static_quantity_literals;

    constexpr 
    refined<is_even, quantity_t<meter_t, int>> r = 2_m;
//  ^~~~~~~ ^~~~~~~  ^~~~~~~~~~~~~~~~~~~~~~~~      ^~~~
//  |       |        |                             |
//  |       |        refined type                  UDL for static_quantity     
//  |       |                                    
//  |       refinement predicate
//  |
//  refinement type

    // Error: `is_even` is not satisfied:
    constexpr refined<is_even, quantity_t<meter_t, int>> error_ = 3_m;

}

ポイント

  • refinedはリテラル(static_quantity)しか受け取れない
  • メタ関数として与えられた述語がtrueを返さなければコンパイルエラーになる
  • UDLを使って値を型に埋め込むことができる

おわり

やっぱ可変長テンプレートがあるのがでかいわ。
可変長テンプレート最高やわ。

疲れてきたのでこのへんでやめるわ。

29
13
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
13