C++で行列のなんやかんやを実装した話

これは最近実装したやつの解説です。主に自分が忘れたときのための備忘録として書いていますから他の人が読むにはあんまり親切ではないかもしれません。

静的なのと動的なの

C++のテンプレートはとても強力なので大抵のことはテンプレートで解決できる。行列を使って何かを計算したいとき、行列のサイズ(型)*1が静的でよい場合と動的であってほしい場合がある。例えばシャミアの秘密分散法などなどで連立一次方程式を解く場合には行列のサイズが動的であると便利だ。しかし、ゲームなどでオブジェクトの描画位置を変えたいというときには3次の正方行列でよいし、ヒープにメモリを確保する必要もない。だからといって静的なサイズの行列と動的なサイズの行列で全く別のクラスにしてしまうのは面倒だし、どちらの行列も同じように扱えるほうがよいに決まっている。なので作った。

github.com

作り方

detection idiomでSizeを制約

まずは定義だけ見てみましょい。

template<class, class, class = void>
class basic_matrix;

template<class T, class Size>
class basic_matrix<T, Size, std::enable_if_t<is_size_spec_v<Size>>> {
    ...
};

Sizeというテンプレート引数にはこちらで用意したサイズ指定子以外を指定されたくない。そのため、detection idiomで、サイズ指定子以外が指定されたbasic_matrixが実体化されないようになっている。

is_size_spec_v<Size>というのがSizeがサイズ指定子かどうかを判定してbool値を返すメタ関数だ。C++ではテンプレートの実体化の優先順位が決まっていて、完全特殊化>部分特殊化>プライマリーテンプレートの順に実体化が試みられる。is_size_spec_v<Size>trueに評価されるとき、部分特殊化がwell-formedになるのでそれが実体化される。またis_size_spec_v<Size>falseになるときはプライマリーテンプレートが実体化される (が、定義がないのでコンパイルエラーになる)。

これでbasic_matrixの実体化については終わり。次は便利な関数を生やしながらそいつらを静的なサイズのやつと動的なサイズの奴の両方に適用できるようにしていく。

メンバ関数とゆかいな仲間たち

サイズ指定子

まずは先ほど紹介しそびれたサイズ指定子について話そう。サイズ指定子は行列のサイズが静的か動的かを選ぶときに指定する。サイズが静的な場合は、実際のサイズの情報もサイズ指定子が持つ。また、basic_matrixは各成分を保存するコンテナにサイズ指定子で宣言されるコンテナ型を使う。静的な場合はC++17の時点でconstexprにふるまうstd::array、動的な場合はstd::vectorを使う。

// 固定長
template<size_t Row, size_t Column>
struct fixed_length final : detail::size_base {
    static constexpr size_t row_size = Row;
    static constexpr size_t column_size = Column;
    template<class T>
    using container_type = std::array<T, row_size * column_size>;
};
// 可変長
struct variable_length final : detail::size_base {
    template<class T>
    using container_type = std::vector<T>;
};

演算可能性

行列はそのサイズによって定義される演算が異なる。たとえば違うサイズの行列には和は定義されないし、左辺の列の数と右辺の行の数が異なれば積は定義されない。ユーザーがある演算を試みた時、それがコンパイル時に演算可能か不可能かが分かるのならばコンパイル時にお知らせしたいと思うのが人間だ。ただし、basic_matrixは動的にサイズを変更できる場合もあるので演算可能性はtrue, falseの二値では表すことも出来ない。よって以下のような三値の条件値を定義した。

enum class condvalue {
    no, yes, maybe
};

例えば、basic_matrix<T, fixed_length<2, 2>>basic_matrix<T, fixed_length<2, 2>>の和を求めたいときはcondvalue::yes

basic_matrix<T, fixed_length<2, 2>>basic_matrix<T, fixed_length<2, 3>>の和を求めたいときはcondvalue::no

basic_matrix<T, fixed_length<2, 2>>basic_matrix<T, variable_length>の和を求めたいときはcondvalue::maybeを返すようなメタ関数を書けば、コンパイル時にエラーを予期したり、コンパイル時エラーにしたりできる。

例えば和

以上を踏まえて、戯れに和を計算する演算子を定義しよう。

template<class T, class Size>
class basic_matrix<T, Size, std::enable_if_t<is_size_spec_v<Size>>> {
    ...
    ...
    template<class U, class S>
    [[nodiscard]]
    friend constexpr auto operator+(const basic_matrix& a, const basic_matrix<U, S>& b)
        noexcept(is_fixed_length_v<Size>&& is_fixed_length_v<S>)
        ->std::enable_if_t<(detail::add_possibility_v<Size, S> > detail::condvalue::no), basic_matrix<std::common_type_t<T, U>, detail::add_possibility_t<S, Size>>>
    {
        basic_matrix<std::common_type_t<T, U>, detail::add_possibility_t<S, Size>> res;
        // 分岐はコンパイル時だが、副文の実行は実行時
        if constexpr (is_variable_length_v<detail::add_possibility_t<Size, S>>) {
            if (a.size() != b.size()) throw std::domain_error("two matrixes that have different size cannot be added each other.");
            res.resize(a.size().first, a.size().second);
        }
        basic_matrix::add(a, b, res);
        return std::move(res);
    }

};

まずはadd_possibility_vというメタ関数だが、これは先に述べた和が定義できるかどうかをno, yes, maybeの三値で返してくれるメタ関数だ。 ここではそのメタ関数で右辺を制約している。戻り値の型を後置する関数宣言構文なのでぱっと見わかりにくいかもしれないが、std::enable_if_tの第一引数には和が定義できる条件になっている。また、動的なサイズの行列の場合、constexpr if文で分岐し、動的に和が定義できるかチェックしている。

このように各演算ごとに定義できるかどうかを三値で返すメタ関数を書きまくれば他の演算についても同じ手法で演算子メンバ関数を書くことができる。おそらくこれが一番楽な方法だと思う。

まとめ

やっぱりC++はなんでもできる。

*1:以降型と書くとC++の型と紛らわしいため行列の型については行列のサイズと書く