2012-03-17

C++11の新機能によるインターフェースの共通化

朝、目が覚めてぼんやりと考え事をしていた。C++11の新機能をどのように活用できるかということについてだ。唐突に、inheriting constructorsはすごく役に立つということに気がついた。

まず、できるだけインターフェースは統一したい。たとえば、std::vectorにT型の要素を入れるのならば、std::vector<T>を使いたい。しかし、もしTがユーザー定義のクラス、MyClassの場合には、必ず、独自のアロケーター、MyAllocを指定したい場合、インターフェースの統一は厄介だ。

std::vector< MyClass, MyAlloc> v ;

古き良きtypedef名を使うという手がある。

using MyVector = std::vector< MyClass, MyAlloc > ;

そして、ユーザーには、std::vector<MyClass>の代わりに、必ずMyVectorを使うようにコード規約で定める。

問題は、誰か一人でも、std::vector< MyClass>と書いてしまったならば、デフォルトテンプレート実引数であるstd::allocator<MyClass>が使われてしまう。コンパイルエラーにはならない。コンパイルエラーにならないコーディング規約は守りにくい。コンパイルエラーにならないのだからチェックも難しい。もちろん、お好きなスクリプト言語でソースコードの静的解析を行うことはできる。しかし、コンパイル時に、ソースコードに対して独自のコード規約確認用のスクリプトを走らせなければならない。そんなビルドシステムのメンテナンスは大変だ。そもそも、テンプレートの場合はどうするのだ。

template < typename T >
void f( T t )
{
    // TはMyClassかもしれない
    std::vector<T> v ;
    v.push_back(t) ;
}

この場合、TがMyClassであるかどうかは、コーディング時には分からない。コンパイル時にしか分からない。したがって、メタプログラミングによるstatic ifが必要になる。幸い、C++11の標準ライブラリにはメタプログラミングに便利なメタ関数が用意されているので、誰でもお手軽にメタプログラマーになれる。

template < typename T >
void f( T t )
{
    typename std::conditional<
        std::is_same<T, MyClass>::value,
            std::vector<T, MyAlloc>,
            std::vector<T>
    >::type v ;
}

いくら標準ライブラリに便利なメタ関数が含まれているからといっても、このコードをスラスラと書くためには、やはり自前でstd::conditionalとかstd::is_sameを実装できる程度の知識が必要になる。私のようなC++情報強者ならともかく、一般ピープルには難しい。第一、いくら知識があるからといって、こんなメタプログラミングを毎回書くのは面倒だ。そこで、メタプログラミングを書くのはライブラリ作者に任せる。

namespace hito {
template < typename T >
struct make_vector
{
    using type =  typename std::conditional<
        std::is_same<T, MyClass>::value,
            std::vector<T, MyAlloc>,
            std::vector< T >
    >::type ;
}

これにより、ユーザーはどのような型Tに対しても、hito::make_vector<T>::typeを使えばよくなる。しかし、これもやはり面倒だ。それに、std::vectorのインターフェースから離れてしまった。そこで、C++11のテンプレートエイリアスの出番だ。これを使うと、ユーザーがnested typeを書かなくても良くなる。

namespace hito {

template < typename T >
using vector = 
    typename std::conditional<
        std::is_same<T, MyClass>::value,
            std::vector<T, MyAlloc>,
            std::vector< T >
    >::type ;
}

これにより、ユーザーはhito:vector<T>を使えばよくなる。だいぶstd::vectorのインターフェースに近づいた。しかし、インターフェースの名前空間が異なる。それに、依然としてユーザーは、std::vector<MyClass>と書くことができてしまう。コンパイラーはstd::vectorを使ったことに対して、何の文句も言わない。さて、どうするか。

実は、ほとんどの標準ライブラリは、ユーザー定義型に対して特殊化することが、規格上明確に許されている。したがって、MyClassに対する特殊化を付け加えてしまえばいいのだ。C++11では、Inheriting Constructorがあるので、これを簡単にしてくれる。

namespace std
{
    template <  >
    struct vector< MyClass, std::allocator<MyClass> > : public vector< MyClass, MyAlloc >
    {
    public :
        using vector< MyClass, MyAlloc >::vector ;
    } ;
}

おそらく、このコードは動くはずである。Inheriting constructorを実装しているコンパイラーがないので、現時点では確かめられないが、規格の解釈を誤っていなければ、正しい。

これにより、ユーザーは型を気にせずに、std::vectorを使うことができる。もちろん、ユーザーが明示的にアロケーターを指定した場合を防ぐことはできないが、そういう明示的な場合を気にする必要はないだろう。

ちなみに、Inheriting constructorを使えない場合、C++03ならば、自前でただ単に基本クラスにデリゲートするだけのコンストラクターの書かなければならない。C++11の場合、ユーザー定義のコピーコンストラクターがあれば、暗黙のコピー代入演算子の宣言はdeprecatedであり、ユーザー定義のムーブコンストラクターがあれば、暗黙のムーブ代入演算子は宣言されないので、代入演算子もデリゲートする必要がある。その数、C++11では何と12個。もちろん、規格を調べて正しい方法で宣言しなければならない。この際、必ず、規格を参考にしなければならない。自分の使っているコンパイラーの実装を参考にしてはならない。なぜならば、規格は、STLの実装においてメンバー関数のシグネチャを一致させることを要求していない。ただ、規格の指定するシグネチャで関数を呼び出した場合、正しく動くことが要求されているのみである。[17.6.5.5]

何にせよ、Inheriting constructorなしでは不必要に難しい。

と、書いていて思ったのだが、やはりテンプレートエイリアスあたりで止めておいたほうが正解かもしれない。

No comments: