[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は色々遊べて面白い。……面白いが、このくらい標準ライブラリでなんとかしてくれよと思わなくもない。