マクロレス型安全printf

もうみんなconstexprに飽きてしまったのか、ほとんど文献がないのでメモ程度に。 私が考えたわけではなく、 constexpr で テンプレートメタプログラミング - TXT.TXT に書いてあったことを試してみたというだけです。 紹介する実装は、コンセプトの確認にとどまっており、完全な実装というわけではありません。 また、静的型付けにこだわらなければ、boost.printfは変な型が来た時に実行時エラーにしてくれるらしいですが、せっかくC++でやっているのにと感じてしまうかもしれません。実際、静的型付けを行うiostreamでは実行時型エラーは起きないため、利便性と引き換えに安全性が失われてしまっているという感じがあります。

Cプリプロセッサマクロは基本的にC++erから嫌われているので、それを使わないで純粋なC++の文法のみで書けるのは一定の成果なのですが、特に読みやすくなるわけではありません。

本題と関係ない話

Twitterとかマストドンとかってすごく検索性が低いなぁと感じます。特に、空リプ(空中リプライ、話題の引用がない会話)みたいなものは運よく検索で見つかってもほとんど情報を得られないです。 マストドンに至っては、この前に流行った時に立った草の根インスタンスはほとんど消滅してしまっています(おそらく、AWSなどを使うと最初の一年間は無料でできるので、それが終わるタイミングで消滅したのでしょう)。

そういうところに技術的な話をうずめてしまうのが悲しいので、ブログみたいな少しは検索性のあるところに書き留めておこうと思って書いています。

ltmpcの時にいろいろなブログ記事にお世話になったから……。

実際のコード

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ユーザー側では、

print( []{return"%d\n";}, a );

のように書けます。型が正しくないコードを書くと、TMPでちゃんとコンパイルエラーになってくれるはずです(ただし、実装されているのは%d%c%p%%くらいなので、それ以外の書式指定文字列だと書式指定文字列として認識されず、「引数が多いよ!」というコンパイルエラーになると思います)。

少し不思議な点

この方法の肝心な点は、「ラムダ式でくるめば値(項)を型に変換できる」ということです。書式指定文字列は全てconst char*の型になってしまい、内容によって型が変わるということはありません。 しかし型安全なprintfを実現するためには、書式指定文字列ごとに別の型になってくれないと型検査ではじけません。逆に書式指定文字列ごとに別の型になっていれば、TMPでどうにかできるはずです。

ここでラムダ式の、「それぞれ固有の型になる」という特性が活用できます。単にconst char*を返すだけのラムダ式であっても、書式指定文字列が違えば別の型になってくれて目的が達せられます(もっとも、書式指定文字列が全く同一でも別の場所に書くだけで異なる型になってしまいますが……)。

ここで不思議なのは、

template<class Str>
auto ToFormatString( Str str ) -> decltype( ToFormatString_emptyCheck<str()[0]>{}( str ) );

のようなコードで、仮引数strメンバ関数呼び出しをした結果をToFormatString_emptyCheck構造体の非型テンプレート引数にしても普通にコンパイルできる点です。 実際のところ、Str型のどんなオブジェクトからも同じ結果が返ってくるのでこれは問題にはなりませんが、strにアクセスした時点ではread of non-constexpr variable 'str' is not allowed in a constant expressionというコンパイルエラーにならないのはちょっと不思議です。 ちなみに、ラムダ式や自作関数オブジェクトにして、内部状態を持たせ、operator()内部でその内部状態に依存する分岐を行わせようとすると確かにread of non-constexpr variable 'str' is not allowed in a constant expressionというコンパイルエラーが発生します。

つまり、this->operator()()を行った時点ではthisに触ったことにはならず、this->xみたいなメンバ変数にアクセスした時点でread of non-constexpr variable 'str' is not allowed in a constant expressionコンパイルエラーになるのですね。