2012-02-21

ユーザー定義リテラルの応用

本の虫: ユーザー定義リテラルのすべて
本の虫: ユーザー定義リテラル補足

先日、ユーザー定義リテラルについて全てを解説した。すでに、十分実験に耐えるコンパイラーもある。しかし、どうもユーザー定義リテラルは使われていない。そこで、ユーザー定義リテラルの活用法を考えて見ることにした。

自作クラスのリテラル

まず最も簡単に思いつくのは、自作クラスのリテラルを作ることだ。たとえば、任意の精度の演算を実現する、架空の整数クラス、bigintを考える。

bigint x("10854312124826591996706630") ;
bigint y("9809954364263402890285234523") ;
bigint z = x + y ; 

変数というのは素晴らしいものだが、やはり我々は、時には直接ハードコードした値を書きたいものである。これを従来の関数とユーザー定義リテラルで実現すると、どうなるだろうか。まずは宣言から。

// 従来の関数
bigint to_bi( std::string val ) ;

// ユーザー定義リテラル
bigint operator "" _bi( char const * str ) ;

ユーザー定義リテラルの宣言は少し見慣れぬが、まあ、これはライブラリ側の実装なので、多少汚くても問題はない。では、ユーザー側はどうか。

// キャスト記法
bigint x = bigint("10854312124826591996706630") + bigint("9809954364263402890285234523") ;

// 従来の関数
bigint x = to_bi("10854312124826591996706630") + to_bi("9809954364263402890285234523") ;

// ユーザー定義リテラル
bigint x = 10854312124826591996706630_bi + 9809954364263402890285234523_bi ;

微妙なところだ。結局、unsigned long long intで表現できない精度の整数は、文字列という形でわたさなければならない。キャスト記法や従来の関数に比べて、括弧を記述しなくても良くなった。しかし、やはりダブルクオートを記述しなくてはならない。読みやすくはなっていると思うた。

単位系の構築

我々は様々な単位を使う。例えば、長さは、メートルが基準だが、キロメートルやセンチメートルなどといった単位も使う。これをいちいち変換するのは面倒だから、ユーザー定義リテラルで自動的に変換させるというのはどうか。

constexpr long double operator "" _km ( long double value ) { return value * 1000.0L ; }
constexpr long double operator "" _m ( long double value ) { return value ; }
constexpr long double operator "" _cm ( long double value ) { return value / 100.0L ; }
constexpr long double operator "" _mm ( long double value ) { return value / 1000.0L ; }

constexpr long double d = 1.0_km + 500.0_m ;

ユーザー定義リテラルはconstexprにできるので、定数もバッチリだ。

constexprを外した上で実行時チェックを行うだとか、long doubleではなくクラスなどを返し、たとえば長さと重さの加算を禁止したり、あるいは逆に、別の単位の演算、例えば、長さ割る時間が速度クラスを返したりなどと、応用することもできる。

メタプログラミングのタグ

メタプログラミングでは、実行時オブジェクトをタグとして使うこともある。これは、オブジェクトではなく、そのオブジェクトの型を利用している。たとえば、std::bindだ。

void f( int x, int y ) { }

int main()
{
    using namespace std::placeholders ;
    auto func = std::bind( f, _2, _1 ) ;
    func( 1, 2 ) ; // f( 2, 1 )として呼ばれる
}

ここで使っている、_1, _2というのは、std::placeholders名前空間で宣言されているオブジェクトである。このオブジェクトどういうものなのかということは、ユーザーは気にする必要がない。実際のところ、実装ですらオブジェクト自体は気にしない。_1, _2の具体的な型は未規定であるが、重要なことは、実装は_1, _2の型を区別することができるようになっている。そのため、ユーザーが何番目の引数にどれを渡したかということを判断でき、operator ()を呼び出した時に渡された実引数を、実際の関数呼び出しの際に、正しくマップすることができる。

そう、型さえ違っていれば、何でもいい。だから、例えばこういう実装でもいいわけだ。

// 実装の一例
// 規格上、型は未規定であり、実装によって都合のいい型が使われる。
namespace std { namespace placeholders {
    template < unsigned long long int > struct holder { } ;
    extern holder<1> _1 ;
    extern holder<2> _2 ;
    // ...
} }

ということは、ユーザー定義リテラルの1_とか2_に対して、対応する型を返してやればいいということになる。しかし、これが結構厄介だ。というのも、通常の関数は使えないからである。通常の関数の戻り値の型は、宣言した時点で決まっている。ユーザーが使った時点で型を決定するには、インスタンス化が必要である。インスタンス化のためには、テンプレートを使わなければならない。では、テンプレートは・・・。

ユーザー定義リテラルのテンプレートは、C++の文法マニアでなければ使いこなせない。しかし、言語マニアとプログラマーとして優れているかどうかは、別問題だ。無論、本物のプログラマーは、使っているプログラミング言語の文法を理解するべきである。しかし、大多数のプログラマーは、言語マニアとなるべきではない。普通のプログラマーは本物の問題を解決するべきなのだ。

偉そうな話はこの辺でやめて、具体的に言おう。問題が多すぎるのだ。ユーザー定義リテラルのオーバーロード演算子のテンプレートは、整数リテラルと浮動小数点数リテラルを区別しないし、その中身も区別しない。

template < char ... Chars >
std::string operator "" _to_string( )
{
    std::string buf ;
    for ( auto c : std::initializer_list<char>{ Chars... } )
    {
        buf.push_back( c ) ;
    }
    return buf ;
}

int main()
{
    std::cout << 0xabcdefABCDEF_to_string << std::endl ;
    std::cout << 01234567_to_string << std::endl ;
    std::cout << 3.141592_to_string << std::endl ;
    std::cout << 123e456_to_string << std::endl ;
}

当然、16進数リテラルや8進数リテラルも、プレフィクスまで含めて文字としてテンプレート実引数に渡されるし、同じ関数テンプレートに、浮動小数点数リテラルも渡される。これを防ぐ方法はない。だから、std::bindに使うとすれば、ユーザー側に10進数整数リテラルのみ使うよう申し送るか、あるいは自前でエラーチェックをするか。

もちろん、戻り値の型を指定するには、テンプレートメタプログラミングが必要だ。

しかも、関数テンプレートに渡されるのは、Variadic Templatesのテンプレート実引数としてのcharの塊である。何とかしてこれを数値に変換しなければならない。atoiは、プログラマーなら誰でも一度ぐらいは実装したことがあるだろう。しかし、テンプレートメタプログラミングやconstexpr関数でatoiを書いた人間はどれだけいるだろうか。std::tupleを使えば、ランダムアクセスは可能である。しかしループはどうしようもない。

もちろん、実装は可能である。再帰はプログラミングの初歩に学ぶことだ。ただ、ちょっとめんどくさいだけだ。試しに、gccのstd::bindの実装で動くものを書いてみたが、コードが無駄に長くなったのでここには貼らない。私が書けたのだから、誰にだって書けるはずだ。

しかし、std::bindの場合、そこまでして_1を1_にする積極的な理由がないのだ。現実的に使われる関数の引数の数などたかが知れている。千個も二千個も引数を渡したりしない。ライブラリ実装者からも、ユーザーからも、あまり利点のない機能である。それに、1_とやって返されるのは、値である。型ではない。関数の実引数としてのタグ以外の、汎用的なテンプレートメタプログラミング用途で使うには、decltypeを利用して、型にしなければならない。しかし、decltype(1_)と書くなら、本来の目的が損なわれてしまう。これだけのためにこんな苦労をするのは馬鹿げている。しかも、浮動小数点数リテラルを間違えて与えてしまう場合もあるのだ。従来のテンプレート、つまり、std::integral_constant<int, 1>のような指定なら、そんな問題は起こらないし、必要ならばconstexpr関数テンプレートを使って、to_int<1>()という形にすることも可能だ。この場合、ちゃんと非型テンプレートとして渡されるので、ユーザー定義リテラルのように文字の塊として処理する必要もない。本末転倒だ。

私はユーザー定義リテラルが嫌いだ。ユーザー定義リテラルなしでは極端に冗長なコードになるということもない。見た目には簡単なコードである。しかし、ユーザー定義リテラルを正しく実装するには、C++11の文法を本当に理解していなければならない。しかし、大多数のプログラマーは、言語の文法を表面上でしか理解していないのだ。普通は、それで十分なのだ。

2 comments:

Anonymous said...

  bigint operator"" _bi(char const *, std::size_t)

ではなくて、

  bigint operator"" _bi(char const *)

を使えば、引用符は不要では?

江添亮 said...

あ、そうか。
リテラルが文字列として扱われるから長さは関係ないのか。