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

シェアする

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

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

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

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

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

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

それを紹介します。

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

概要

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

// 冒頭略
int main(void) {
	int num = 2;
	double pi = 3.14;
	string str = "str";
	char *chararr = "chararr";
	vector<int> vec{ 1,1,2,3 };

	dump(num, pi, str, chararr, vec);

	return 0;
}

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

  num, pi, str, chararr, vec :[62:main]
    2, 3.14, str, chararr, {1, 1, 2, 3} 

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

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

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

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

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

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

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

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

// vector出力
template<typename T>
ostream& operator << (ostream& os, vector<T>& vec) {
	os << "{";
	for (int i = 0; i<vec.size(); i++) {
		os << vec[i] << (i + 1 == vec.size() ? "" : ", ");
	}
	os << "}";
	return os;
}

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

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

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

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

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


// pair出力
template<typename T, typename U>
ostream& operator << (ostream& os, pair<T, U>& pair_var) {
	os << "(" << pair_var.first << ", " << pair_var.second << ")";
	return os;
}
// map出力
template<typename T, typename U>
ostream& operator << (ostream& os, map<T, U>& map_var) {
	os << "{";
	for (map<T, U>::iterator itr = map_var.begin(); itr != map_var.end(); itr++) {
		os << "(" << itr->first << ", " << itr->second << ")";
		itr++;
		if(itr != map_var.end()) os << ", ";
		itr--;
	}
	os << "}";
	return os;
}
// set 出力
template<typename T>
ostream& operator << (ostream& os, set<T>& set_var) {
	os << "{";
	for (auto itr = set_var.begin(); itr != set_var.end(); itr++) {
		os << *itr;
		++itr;
		if(itr != set_var.end()) os << ", ";
		itr--;
	}
	os << "}";
	return os;
}

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

可変長引数テンプレート

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

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

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

// 再帰の終端。引数が0個の場合を担当。改行を出力。
void dump_func() {
	cout << endl;
}
// 可変長引数。引数が1つ以上存在する場合を担当。
// 最初の引数をHead、残りをTailとして切り離すことを再帰的に行う。
template <class Head, class... Tail>
void dump_func(Head&& head, Tail&&... tail)
{
	cout << head;
	if (sizeof...(Tail) == 0) {
		cout << " ";
	}
	else {
		cout << ", ";
	}
	dump_func(std::move(tail)...);
}

可変長引数テンプレートの要素数は、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つの文字列に置き換わります(複数の文字列の列にはなりません)。

#define dump(...) cout<<"  "; \
cout<<#__VA_ARGS__<<" :["<<__LINE__<<":"<<__FUNCTION__<<"]"<<endl; \
cout<<"    "; \
dump_func(__VA_ARGS__)

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

  num, pi, str :[62:main]
    2, 3.14, str

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

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

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

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

トークン結合演算子

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

記号は ## を使います。

例えば、

#define PRINTF(x) x##printf

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

単純に#define PRINTF(x) xprintf

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

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

単純に

// 提出時はコメントアウト
#define DEBUG_

#ifdef DEBUG_
#define DEB
#else
#define DEB //
#endif

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

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

// 提出時はコメントアウト
#define DEBUG_

#ifdef DEBUG_
#define DEB
#else
#define DEB /##/
#endif

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

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

#define dump(...) DEB cout<<"  "; \
cout<<#__VA_ARGS__<<" :["<<__LINE__<<":"<<__FUNCTION__<<"]"<<endl; \
cout<<"    "; \
dump_func(__VA_ARGS__)

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

完成!

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

最終的なコードは以下のようになります。

#include <iostream>
#include <string>
#include <vector>

using namespace std;

// 変数ダンプ先。coutかcerr
#define DUMPOUT cout

// 提出時はコメントアウト
#define DEBUG_

#ifdef DEBUG_
#define DEB
#else
// DEB と打つとデバッグ時以外はコメントアウトになる
#define DEB /##/
#endif

// 変数ダンプ用マクロ。デバッグ時以外は消滅する
// 引数はいくつでもどんな型でも可(ストリーム出力演算子があればOK)
#define dump(...) DEB DUMPOUT<<"  "; \
DUMPOUT<<#__VA_ARGS__<<" :["<<__LINE__<<":"<<__FUNCTION__<<"]"<<endl; \
DUMPOUT<<"    "; \
dump_func(__VA_ARGS__)

// デバッグ用変数ダンプ関数
void dump_func() {
	DUMPOUT << endl;
}
template <class Head, class... Tail>
void dump_func(Head&& head, Tail&&... tail)
{
	DUMPOUT << head;
	if (sizeof...(Tail) == 0) {
		DUMPOUT << " ";
	}
	else {
		DUMPOUT << ", ";
	}
	dump_func(std::move(tail)...);
}

// vector出力
template<typename T>
ostream& operator << (ostream& os, vector<T>& vec) {
	os << "{";
	for (int i = 0; i<vec.size(); i++) {
		os << vec[i] << (i + 1 == vec.size() ? "" : ", ");
	}
	os << "}";
	return os;
}

int main(void) {
	int num = 2;
	double pi = 3.14;
	string str = "str";
	char *chararr = "chararr";
	vector<int> vec{ 1,1,2,3 };

	dump(num, pi, str, chararr, vec);

	return 0;
}

出力:

  num, pi, str, chararr, vec :[62:main]
    2, 3.14, str, chararr, {1, 1, 2, 3} 

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

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

では。

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

シェアする

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

フォローする