競プロ用C++変数ダンプ関数・マクロを作ったよ

Pocket

こんばんは、かなり久しぶりの投稿になってしまいました。ふるやんです。

今日が何リスマス何ブだかは知りませんが、僕は元気にプログラムを書いています。

さて、CODE FESTIVAL 2017の参加記やICPCアジア予選つくば大会の参加記も書きたいのですが、今日は全く関係のない記事です。

私はVisual Studioを使っているのに変数ウォッチ機能的なやつを使いこなせず、いまだにprintfデバッグ(正確にはcoutデバッグ)をしています。

最近、このcoutデバッグをより効率的に行うために使える知識を見つけたので、変数ダンプ用関数・マクロを作成しました。

それを紹介します。

ちなみに変数のダンプとは、変数の中身を標準出力や標準エラー出力に出力すること、としています。PHPのvar_dump()みたいな。

2019/03/22 追記:

トークン結合演算子の項目について、VC++以外では動かないようなので別の記述方法を追記しました。

また、コンパイルオプションで define する方法も追記しました。

概要

以下のような機能を作ります。

これを実行すると、次のように出力されます。(出力先はcoutかcerrか選べます)

dump()の中に、好きな個数好きな型の変数をコンマ区切りで入れると、変数名一覧、dump()のある行番号、dump()が呼ばれた関数、dump()に入れた変数のそれぞれの値、が出力されます。

ダンプする変数の型はストリーム出力演算子”<<“が定義されている必要がありますが、演算子オーバーロードでいくらでも用意できます。

では、これを作っていきましょう。

演算子オーバーロードでvector<T>を出力

参考:http://koyumeishi.hatenablog.com/entry/2016/02/01/152426

まず、vector<int> vec に対して、「cout<<vec」で中身を表示できるようにしましょう。

“<<“は「ストリーム出力演算子」です。「左側に出力ストリーム、右側に変数があるときに、変数を出力し、出力ストリームを返す」というものです。

難しいことは考えずに、以下の記述をすれば大丈夫です。

osにはcoutやcerrが来ます。ファイル出力の場合はofstreamクラスの値が来ますね。

引数(左オペランド)のストリームをそのままreturnすることで、「cout<<a<<b」のような場合に、aを出力した後に「cout<<b」となって、bを出力することができるようになっています。右側から評価されるのですね。

テンプレートを用いることで、任意のvectorに対応できます。vectorやvector、さらにはvector<vector>などにも。

出力は「{1, 2, 3}」のような形になりますが、内容をいじれば括弧や区切りを変えられます。

pair, map, setなども、同じように実装することができます。区切りを末尾要素の後に入れないための処理がちょっと面倒ですが。

上記の参考ページには cin>>vec で配列を一気に受け取る方法も書かれてあります。競プロで便利ですね。

可変長引数テンプレート

参考:https://cpprefjp.github.io/lang/cpp11/variadic_templates.html

テンプレート引数に<class… Ts>の表記を用いると、任意個の型のリストをTsとして扱えます。tupleみたいな感じですね。

関数の可変長引数と組み合わせて、以下のように記述できます。

可変長引数テンプレートの要素数は、sizeof…演算子で得られます。「sizeof…」という記述はなんか不思議ですね。

この関数に dump_func(num, pi, str) などと引数を与えると、「2, 3.14, str」のように一行に出力してくれます。

これだけでも大丈夫ですが、せっかくなのでもっとリッチにしていきましょう。

行番号や関数名を取得する

C++の規格では、「定義済みマクロ」というものがいくつか用意されています。

これは、プリプロセスの段階で上手いこと解釈して良い感じに文字列を置き換えてくれる感じのアレらしいです(勉強不足)。

例えば、適当なところに cout<<__LINE__; と書くと、それを記述した場所の行番号を出力してくれます。

実際には、コンパイルの前段階で、__LINE__ の部分がその位置の行番号に書き換えられます。

__FUNCTION__ だと、その記述が行われている関数名、__FILE__ だと、そのコードが書かれているファイル名が得られます。超便利。Pythonにこんなのありますよね。C++にもあったんだ。

dump_func()の中で cout<<__LINE__; としたいところですが、そうするとdump_func()のある行番号が出力されてしまい、意味がありません。dump_func()を呼び出した元の場所を出力してほしいのです。

そこで、マクロを使います。

#define dump(x) dump_func(x, __LINE__)

とすると、dump(num) を記述した部分が dump_func(num, __LINE__) に書き換えられ、さらに__LINE__がその位置の行番号に書き換えられるので、xの値と行番号が正しく出力されます。

しかし、これだと可変長引数にできません。

そこで、可変長引数マクロです。

可変長引数マクロ関数

関数やテンプレートに可変長引数が存在するように、マクロ関数にも可変長引数が存在します。

#define dump(…) dump_func(__VA_ARGS__, __LINE__)

とすると、dump()が可変長引数マクロ関数として解釈され、その引数パックが__VA_ARGS__によってdump_func()の引数に突っ込まれ、その後に行番号もdump_func()の引数として突っ込まれます。

__VA_ARGS__も定義済みマクロですね。

これで、行番号の出力に成功しました。dump_func(__VA_ARGS__, __LINE__, __FUNCTION__) と定義すると、関数名も出力できますね。

次に、変数名を出力します。

文字列化演算子

マクロ関数のみで使える「文字列化演算子」という特殊な演算子があります。

これは、マクロ関数の引数として与えられたものを文字列に変えるものです。記号は#を使います。

#define TOSTRING(x) #x

とすると、

int num = 2;

cout << TOSTRING(num);

としたときに、「2」ではなく「num」が出力されます。

#define TOSTRING(x) (#x + “:” + to_string(x))

とすると、「num:2」と出力させることができます。便利。

具体的には、#xはマクロ関数の仮引数xとして与えられた表記を””で囲んだものに置き換えられる、という機能があります。

エスケープとかその辺も上手いことやってくれるらしいです(勉強不足)。

これは、__VA_ARGS__にも使えます。

#define dump(…) dump_func(#__VA_ARGS__, __VA_ARGS__)

とすると、dump(num, pi, str) としたときに「num, pi, str, 2, 3.14, str」が表示されます。

この場合、__VA_ARGS__は「num, pi, str」なので、#__VA_ARGS__は「”num, pi, str”」という1つの文字列に置き換わります(複数の文字列の列にはなりません)。

とすると、dump(num, pi, str)としたときに

が表示されます(main関数内の62行目にdump(num, pi, str)を書いた場合)。やったね!

マクロ内では\マークを入れることで改行ができます。改行前に\マークを入れると改行が無視されるのですね。

ちなみに段下げ用のスペース出力は、dump()以外の(正式な)出力と区別しやすくするために入れています。

これだけでも良いのですが、せっかくなので提出時にdump()を無効にできるようにしましょう。全部消すのは面倒ですし、消し忘れがあってWAだと悲しいので。

トークン結合演算子

追記:以下の記述はVC++では動くようですがg++などでは動かないようなので、素直に if(false) を使うのが良いです。

一応、トークン結合演算子の紹介↓

文字列化演算子と同じように、マクロ内でしか使えない演算子に「トークン結合演算子」があります(文字列化演算子はマクロ関数のみですが、こちらは関数でないマクロでも使えます)。

記号は ## を使います。

例えば、

#define PRINTF(x) x##printf

と書き、PRINTF(f)(…) と記述したときに、PRINTF(f) の部分が「f##printf」に置き換わり、##が消えて「fprintf」になります。

単純に#define PRINTF(x) xprintf

とすると、”xprintf”のxがマクロ関数の引数xと同じと解釈されずに、上手くいきません。

これの使い道ですが、「DEB」という記述を、デバッグ時には空文字列に、非デバッグ時(提出時)には「//」にしてコメントアウトに使用したい場面を考えます。

単純に

とすると、「#define DEB //」の「//」がコメントアウトと解釈され、「#define DEB」と同じ扱いになってしまいます。

これを防ぐには、以下のようにすれば良いです。

こうすると、「#define DEB /##/」には「//」が存在しないので、まず「DEB」が「/##/」に書き換えられ、その後にトークン結合演算子が消滅して「//」に変わります。

これを活用すると、dump()マクロを以下のように書けます。

DEBの記述はdump()以外にも使えます。一行だけの記述なら使えますが、複数行のブロックをデバッグ用にしたい場合は、if文などを使用しましょう。

完成(基本形)

さらに、些細なことですが、ダンプ出力をcoutかcerrか選べるようにするため、DUMPOUTをdefineしましょう。

最終的なコードは以下のようになります(pair, map, set の出力もオーバーロードしています)。

出力:

#define DEBUG_ をコメントアウトするか削除すると、dump()は何もしなくなります。

ですが、提出するときにいちいち #define DEBUG_ を消すのは面倒な場合は、次の方法が使えます。

追記:コンパイルオプションでdefine

gcc や clang では、コンパイルオプションでマクロを定義することができます。

コンパイル時に -Dhoge=7 というオプションをつけると、ソースコードで #define hoge 7 とした場合と同じ効果が得られます。

-D[マクロ名](=値) です。値を設定しないことも可能です。

したがって、コンパイル時に -DDEBUG_ というオプションをつけると、#define DEBUG_ と同じ効果が得られるので、上記のデバッグ用の記述が有効になります。

ソースコードから #define DEBUG_ を削除して、コンパイルオプションに -DDEBUG_ も使用しなければ、define していない扱いになるので、デバッグ用の記述が無効化されます。

コンテストで提出したときは、このオプションはつかないので、必ず無効になります。

手元でコンパイルするときだけ、

g++ -std=c++14 -o main -DDEBUG main.cpp

のようにすれば大丈夫です。

これでかなり便利になると思います。

記述は長いですが、非常に便利なので、printfデバッグをする方はぜひ活用してみてください。

では。

Pocket

スポンサーリンク
レクタングル大
レクタングル大

シェアする

  • このエントリーをはてなブックマークに追加

フォローする