[C++]テンプレートテンプレートパラメータの“テンプレート引数の数”を取得することはできるのだろうか。

 例えば次のようなクラステンプレートX、Yがあったとする。

template <class>
struct X {};

template <class, class>
struct Y {};

Xはテンプレート引数が1個、Yはテンプレート引数が2個である。

 このX、Yを次のようなクラステンプレートに与えてみる。このXorYは、与えられたXやYのテンプレート引数の数を知ることはできるのだろうか。

template <template <class...A> class T>
struct XorY
{
    //static constexpr size_t NumOfTArgs = sizeof...(A);これは不可能。
};
//XorY<X>::NumOfTArgs == 1;
//XorY<Y>::NumOfTArgs == 2;
//であってほしい。

 テンプレート引数の数はXなら1個、Yなら2個であるが、XorYのテンプレートテンプレートパラメータは可変長であるため、そのままでは分からない。

 色々と悩んだが、次のようにconstexprな関数のオーバーロードで解決する方法しか思いつかなかった。

template <template <class> class T>
constexpr size_t NumOfTArgs() { return 1; }
template <template <class, class> class T>
constexpr size_t NumOfTArgs() { return 2; }

template <template <class...> class T>
struct XorY
{
    static constexpr size_t NumOfTArgs = NumOfTArgs<T>();
};

次のような部分特殊化ができないかとも思ったが、どうやら不可能なようだ。

template <template <class...> class T>
struct NumOfTArgs;
template <template <class> class T>
struct NumOfTArgs<T> { static constexpr size_t value = 1; };
template <template <class, class> class T>
struct NumOfTArgs<T> { static constexpr size_t value = 2; };
//プライマリテンプレートの仮引数Tと2個の部分特殊化の引数Tとが一致してしまっているためコンパイルエラー。

 テンプレートテンプレートパラメータは不可欠な機能であるが、ちょっと不便な所も多い印象だ。非型テンプレート引数にautoを使えないC++14以前だと融通が効かないのでしばしば苦労するし、上のXorYに非テンプレートなクラスを渡せないのも面倒だ。通常の可変長引数テンプレートは引数0個が許されるのに、テンプレートテンプレートパラメータでは許されないという厄介さ。何とかならないものだろうか。

pybind11の引数の型に注意。

 自作ライブラリのPythonラッパーを作るためにpybind11を導入している私だが、関数引数の型についていくつかのエラーに悩まされたので、備忘録として記しておく。

デフォルト引数の型が適切かどうかはコンパイル時に判定されない。

void func(const std::vector<double>&) {}
m.def("func", &func, "vector"_a = "");

 ある関数funcがあったとして、それのデフォルト引数に空文字列""を指定したとする。空文字列からstd::vectorへの変換はできないので、これはコンパイルエラーになるかと思いきや、ならない。Pythonでの実行時に初めて、型の指定が間違っていて関数を呼べないとエラーメッセージが表示される。
 引数が20個くらいある関数のデフォルト引数の型の誤りに気付けず一時間くらい悩んだ。

引数の暗黙型変換は(自動的には)行われない。

struct A
{
    A(const std::string&) {}
}
void func(A a) {}

py::class_<A>(m, "A").
    def(py::init<>());
m.def("func", &func);

 このようにラップされた関数funcがあったとする。C++ではfuncに対してconst char*やstd::stringを与えた時にコンストラクタを介した暗黙の型変換によって勝手に呼ぶべき関数を判定してくれるが、Pybind11はそのままでは型の変換が行われず、Pythonの実行時に関数呼び出し失敗のエラーが生じる。

func("test")
-> incompatible function arguments. The following argument types are supported...

 暗黙の型変換を有効にするには、次のように予め定義しておく必要がある。

py::implicitly_convertible<std::string, A>();

[C++]配列でない個別の値を一纏めにしてrange-based for loopで走査、編集したいとき。

 先日は複数のコンテナをrange-based for loopで同時に走査する方法を書いたが、今回は配列になっていない同じ型の変数についてループする話。例えばint型の変数i1~i5がある時、i1~i5までをループしたくなったらどうするのか。
 よくある方法としては、波括弧初期化子リストで括ってしまう方法だ。

int i1, i2, i3, i4, i5;
for (int i : { i1, i2, i3, i4, i5 }) { std::cout << i << std::endl; }

 しかしこの方法には欠点がある。この初期化子リストはstd::initializer_listに推定されるが、std::initializer_listは基本的に、値をコピーして配列化したものである。上でループしているiは、たとえconst int&として受け取ったとしても、i1~i5とは別のインスタンスである。したがって、i1~i5を編集することはできない。ついでにいうと、std::initializer_list自体が一時配列であるため編集されることは想定されておらず、基本的にconst_iterator相当の機能しか提供しない。
 またもう一つ困ったことに、先日のBundleRangeやBundleRangeWithIndexにそのまま波括弧で括って与えることが出来ないのも面倒である。これは初期化子リストが必要に応じてstd::initializer_listへと変換されるだけの、型を持たない値の羅列に過ぎないからだ。型を持たない以上テンプレート引数の推定ができず、コンパイルエラーとなる。autoで受け取ることでstd::initializer_listに推定してくれるのは例外的動作なのだ(autoの推定に例外を設けるのなら、テンプレートの推定にも設けてくれればいいのにと思わなくもない)。

//i1~i5をインデックス付きで走査しようとするもコンパイルエラーとなる。
for (auto[index, i] : BundleRangeWithIndex({ i1, i2, i3, i4, i5 })) {}

 これらの問題を解決する単純な方法はない。前者に限定すれば、

for (auto i : { std::ref(i1), std::ref(i2), std::ref(i3), std::ref(i4), std::ref(i5) }) { i.get() = 0; }
for (auto i : { &i1, &i2, &i3, &i4, &i5 }) { *i = 0; }
//これは意図したように動かない。for (const auto& i : { i1, i2, i3, i4, i5 }) { const_cast<int&>(i) = 0; }

などの不格好な方法で実現させられなくはないが、直感的なRange based for loopの使い方と食い違うため、私は好ましいと思わない。3番目は動きそうに見えるが、残念ながら先述したようにstd::initializer_listが保持するのはもとの値のコピーであるため、const_castによって編集しても元のi1~i5には影響を与えない。

 しかしそうなると、もうstd::initializer_listを諦めるしかない。代わりに次のようなものを用意しよう。

namespace detail
{

template <class T, std::size_t N>
class ReferenceArray
{
public:

    template <class ...Args>
    ReferenceArray(Args&& ...args) : mPtrArray{ &std::forward<Args>(args)... } {}

    class Iterator
    {
    public:
        Iterator(typename std::array<std::add_pointer_t<T>, N>::iterator i) : mIterator(i) {}

        Iterator& operator++()
        {
            ++mIterator;
            return *this;
        }
        T& operator*() const noexcept
        {
            return *(*mIterator);
        }
        bool operator==(const Iterator& it2) const
        {
            return mIterator == it2.mIterator;
        }
        bool operator!=(const Iterator& it2) const
        {
            return !(*this == it2);
        }

    private:
        typename std::array<std::add_pointer_t<T>, N>::iterator mIterator;
    };

    constexpr std::size_t size() const { return N; }
    Iterator begin()
    {
        return Iterator(mPtrArray.begin());
    }
    Iterator end()
    {
        return Iterator(mPtrArray.end());
    }

private:

    std::array<std::add_pointer_t<T>, N> mPtrArray;
};

template <class ...Ts>
struct CommonType_impl;
template <class CommonT>
struct CommonType_impl<CommonT, CommonT>
{
    using Type = CommonT;
    static constexpr bool value = true;
};
template <class CommonT, class T, class ...Ts>
struct CommonType_impl<CommonT, T, Ts...>
{
    static constexpr bool value = false;
};
template <class CommonT, class ...Ts>
struct CommonType_impl<CommonT, CommonT, Ts...> : public CommonType_impl<CommonT, Ts...>
{};

}

template <class ...T>
struct CommonType
{
    using Type = typename detail::CommonType_impl<T...>::Type;
    static constexpr bool value = detail::CommonType_impl<T...>::value;
};

template <class ...Args>
detail::ReferenceArray<typename CommonType<Args...>::Type, sizeof...(Args)> HoldRefArray(Args& ...args)
{
    return detail::ReferenceArray<typename CommonType<Args...>::Type, sizeof...(Args)>{ args... };
}

constexpr unsigned int factorial(unsigned int x)
{
    return x != 0 ? factorial(x - 1) * x : 1;
}

int main()
{
    int i1, i2, i3, i4, i5;
    int j = 0;
    for (auto& i : HoldRefArray(i1, i2, i3, i4, i5))
    {
        i = j;
        ++j;
    }
    for (auto i : { i1,i2,i3,i4,i5 })
    {
        std::cout << i << std::endl;
    }
    return 0;
}

 要するに与えた任意個数の変数への参照を持つような配列を一時的に作り、それをループすれば良い。ただし一般に配列は参照型を持つことが出来ない。これはstd::vectorやstd::arrayなども同様である。なので参照型をメンバとして持つクラスを作るか、ポインタ型で持たせなければならない。std::reference_wrapperなどを使ってもいいだろう。今回はポインタにした。

 初期化子リストと違い明確にReferenceArray型でるため、BundleRange関数と組み合わせることもできる。また配列化する変数の型はconst、非const双方に対応している。

int main()
{
    const double lambda1 = 1;
    const double lambda2 = 5;
    const double lambda3 = 9;
    const double lambda4 = 13;
    std::vector<double> y1(20);
    std::vector<double> y2(20);
    std::vector<double> y3(20);
    std::vector<double> y4(20);

    for (auto[l, y] :
         BundleRange(
             HoldRefArray(lambda1, lambda2, lambda3, lambda4),
             HoldRefArray(y1, y2, y3, y4)))
    {
        for (int i = 0; i < 20; ++i)
        {
            y[i] = std::pow(l, i) * std::exp(-l) / factorial(i);
        }
    }
    for (auto[ya, yb, yc, yd] : BundleRange(y1, y2, y3, y4))
    {
        std::cout << ya << " " << yb << " " << yc << " " << yd << std::endl;
    }
    return 0;
}

 HoldRefArrayでlambdaを纏めた配列とyを纏めた配列の2つを作り、それをBundleRangeによって同時に走査している。もちろん編集可能である。例のGnuplotラッパーライブラリをテストしている最中に思いついて作った機能なので、上の例はポアソン分布を計算するものになっている。

 上のHoldRefArray関数は、残念ながら、すべての変数の型が同一であることが要求される。上のconst double型のlambdaの中にしれっと非constなdoubleが混ざったり、基底クラスと派生クラスのポインタを混ぜたりするとコンパイルエラーとなる。このあたりはstd::common_typeが上手く仲介してくれないかと思ったのだが、そういう挙動ではなかった

 range-based for loopは色々遊べて面白い。……面白いが、このくらい標準ライブラリでなんとかしてくれよと思わなくもない。