2014/03/18にTwitterで発生した謎の流れにより、「コンパイル時に型エラーを検出できる型安全なprintf
を作ろう!」という流れに
C言語のprintf
が型安全でないのは有名な話で、その代わりにboost::format
があったりしてboost::format
などでは「ある程度」型安全というか、型が違ったら「実行時に」例外が投げられたりとか、ありますよね?
そういうのではなく、「コンパイル時に」型エラーを検出してしまえ! 的な感じのprintf
を作成しよう! という流れになりました
と、言うわけで今回はC++14(C++11縛りはキツかった)で作成した、というか作成中、というより漸進的に作成されるType safe printfのお話です
まず最初に、リポジトリはこちらです
https://github.com/minamiyama1994/Type-safe-printf
現在char
、int
、double
、char[N]
に対応しています
例えばこんなコードを書くと
#include"TSP/TSP.hpp"
auto main ( ) -> int
{
TSP_PRINTF ( "%c\n" , 'a' ) ;
TSP_PRINTF ( "%s\n" , "hoge" ) ;
TSP_PRINTF ( "%d\n" , 1 ) ;
TSP_PRINTF ( "%f\n" , 0.1 ) ;
TSP_PRINTF ( "%%\n%c\n%s\n%d\n%f\nHello! TSP!\n" , 'e' , "piyo" , 1 , 0.25 ) ;
// TSP_PRINTF ( "%f\n" , "hoge" ) ; // <- Compile Error
}
こんな実行結果になります
a
hoge
1
0.1
%
e
piyo
1
0.25
Hello! TSP!
コメントアウトしてある場所が気になりますね……ちょっと外してみましょう
#include"TSP/TSP.hpp"
auto main ( ) -> int
{
TSP_PRINTF ( "%c\n" , 'a' ) ;
TSP_PRINTF ( "%s\n" , "hoge" ) ;
TSP_PRINTF ( "%d\n" , 1 ) ;
TSP_PRINTF ( "%f\n" , 0.1 ) ;
TSP_PRINTF ( "%%\n%c\n%s\n%d\n%f\nHello! TSP!\n" , 'e' , "piyo" , 1 , 0.25 ) ;
TSP_PRINTF ( "%f\n" , "hoge" ) ; // <- Compile Error
}
こんな実行結果になります、というかコンパイルエラーメッセージが出ます
In file included from example.cpp:1:0:
./include/TSP/TSP.hpp: In instantiation of 'constexpr auto tsp::printf(const T& ...) [with format_type = ftmp::list<ftmp::integral<char, '%'>, ftmp::integral<char, 'f'>, ftmp::integral<char, '\012'> >; T = {char [5]}]':
example.cpp:9:2: required from here
./include/TSP/TSP.hpp:255:55: error: no matching function for call to 'tsp::detail::printf<ftmp::list<ftmp::integral<char, '%'>, ftmp::integral<char,'f'>, ftmp::integral<char, '\012'> > >::func(const char [5])'
return detail::printf < format_type >::func ( v ... ) ;
^
./include/TSP/TSP.hpp:255:55: note: candidate is:
./include/TSP/TSP.hpp:246:26: note: template<class ... T> static constexpr auto tsp::detail::printf_impl_format<ftmp::list<ftmp::integral<char, '%'>,ftmp::integral<char, 'f'>, ftmp::integral<char, ch>...> >::func(double, const T& ...) [with T = {T ...}; char ...ch = {'\012'}]
constexpr static auto func ( double h , const T & ... v )
^
./include/TSP/TSP.hpp:246:26: note: template argument deduction/substitution failed:
./include/TSP/TSP.hpp:255:55: note: cannot convert 'v#0' (type 'const char [5]') to type 'double'
return detail::printf < format_type >::func ( v ... ) ;
^
example.cpp: In function 'int main()':
./include/TSP/TSP.hpp:261:72: error: void value not ignored as it ought to be
tsp::printf < SPROUT_PP_CAT ( format_type , __LINE__ )> ( __VA_ARGS__ ) ( )
^
example.cpp:9:2: note: in expansion of macro 'TSP_PRINTF'
TSP_PRINTF ( "%f\n" , "hoge" ) ; // <- Compile Error
^
make: *** [all] Error 1
これは、%f
という書式指定子に対してchar[N]
、つまりchar
配列が渡されており、型が合っていないからです
と、これだけ書いたら「C++こわっ意味わからん」となる人が大半だと思うので、少々説明を(説明かったるいという人はリポジトリのソースコード直接見て下さい)
使用ライブラリは中3女子氏の作成されたSproutと僕の作ったFTMPです
それぞれconstexpr用のライブラリとTMP用のライブラリです
まず、format文字列はstd::basic_string
などではなくsprout::basic_string
を使用しています
これにより、コンパイル時に「どのような文字列なのか」を取得することが可能となります
更に、この中3女子氏の作成されたSproutにはSPROUT_TYPES_STRING_TYPEDEF
という最強マクロがありまして、これを使うことでsprout::basic_string
の内部を型として得ることが出来ます
どういうこっちゃ! という人はこちらを御覧ください。要するに「コンパイル時に型レベルで文字列操作が可能になる」ということです
こうなればもうこっちのものです、format文字列がコンパイル時に型として得られたのですから、それを元に可変長引数部分と突き合わせ、整合性がとれているかどうかをチェックするだけです
一つ注意が必要なのが、ここではstd::cout
などの大半の標準ライブラリは使えません、何故かと言うと処理の大半がconstexpr
関数でなされ、そしてそういったライブラリはconstexpr
関数内では使えないためです
というわけで、直接出力するわけには行きません
そこで、「constexpr
として構築できるけど非constexpr
なoperator()
を呼び出したら出力が行われるクラス」を作り、それを結果として返すようにします。HaskellのIO Monad
が分かる人は、それをイメージしてもらえるとわかりやすいかもしれません
ここまでの流れをまとめると
-
sprout::basic_string
を元にformat引数を型に変換して取得する - format指定と可変長引数部分を突き合わせる
- 出力を行う関数オブジェクト(これそのものは
constexpr
として構築可能) - 結果の関数オブジェクトの
operator()
を呼び出し、出力が完了
という流れです
ここまでの流れは手で書くと煩雑なのでTSP_PRINTF
というマクロにまとめています
概ね、以上です
……と、以上の記事は寝る前にサクッと書いたものなのですが、起きてから気がついた、これ、format文字列を型に落とし込めた時点でもうconstexpr
にする意味はもうないのでは……?
……い、いや、constexpr
だからって誰かが損するわけでもないし! 別にいいし!
では