LoginSignup
14

More than 5 years have passed since last update.

型安全なprintf -- 如何にしてコンパイル時に型エラーを検出するか --

Posted at

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
現在charintdoublechar[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として構築できるけど非constexproperator()を呼び出したら出力が行われるクラス」を作り、それを結果として返すようにします。HaskellのIO Monadが分かる人は、それをイメージしてもらえるとわかりやすいかもしれません
ここまでの流れをまとめると

  1. sprout::basic_stringを元にformat引数を型に変換して取得する
  2. format指定と可変長引数部分を突き合わせる
  3. 出力を行う関数オブジェクト(これそのものはconstexprとして構築可能)
  4. 結果の関数オブジェクトのoperator()を呼び出し、出力が完了

という流れです
ここまでの流れは手で書くと煩雑なのでTSP_PRINTFというマクロにまとめています
概ね、以上です


……と、以上の記事は寝る前にサクッと書いたものなのですが、起きてから気がついた、これ、format文字列を型に落とし込めた時点でもうconstexprにする意味はもうないのでは……?
……い、いや、constexprだからって誰かが損するわけでもないし! 別にいいし!

では

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14