本の虫: ユーザー定義リテラルのすべて
本の虫: ユーザー定義リテラル補足
先日、ユーザー定義リテラルについて全てを解説した。すでに、十分実験に耐えるコンパイラーもある。しかし、どうもユーザー定義リテラルは使われていない。そこで、ユーザー定義リテラルの活用法を考えて見ることにした。
自作クラスのリテラル
まず最も簡単に思いつくのは、自作クラスのリテラルを作ることだ。たとえば、任意の精度の演算を実現する、架空の整数クラス、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:
bigint operator"" _bi(char const *, std::size_t)
ではなくて、
bigint operator"" _bi(char const *)
を使えば、引用符は不要では?
あ、そうか。
リテラルが文字列として扱われるから長さは関係ないのか。
Post a Comment