競プロでバグ取るのにcoutを何回も書くの,やってられない

この記事は, Muroran Institute of Technology Advent Calendar 2018 4日目 の記事です.

※投稿日は1/4ですが,12/4も1/4も変わらないので実質セーフ

はじめに

不思議なことに,最近周りで競技プログラミング(競プロ)が流行り始めています.
競プロは素早く正確にコードを書くことが大切です.
しかし,私はそれ以上にいかにしてバグを素早く見つけて修正することができるかが重要だと考えます.
バグを見つける代表的な手法として「printfデバッグ」があります.
これはその名の通り,変数を出力することによって異常値を探し,異常個所を特定する手法です.
非常に簡単な手法ですが,競プロのように常に焦る環境に置かれると,出力文を書くだけでも手間だと感じます.
特にリストを出力する場合は逐一for文を書く必要があり,大きな時間のロスとなります.
そこで本記事では,簡潔に記述できて,かつデバッグしやすいprintfデバッグ用マクロを組んでいきます.

目標

本記事では以下のような機能を持つマクロを作成します.

  • 簡単に複数変数を表示
  • 関数一つであらゆる変数を表示
  • 値と同時に変数名も表示

理想の動作を以下に示します.
コード

// 色々な変数
int num = 2;
std::string str = "hello";
std::vector<int> vec = {1,2,3};
std::list<char> lst = {'a','b','c'};
std::vector<std::vector<double>> mat = {{1.1, 2.3}, {5.6, 1.2}};

// 作成したデバッグマクロ
dump(num, str, vec, lst, mat);

出力

num : 2
str : "hello"
-- vec --
1 2 3
-- lst --
a b c
-- mat --
1.1 2.3
5.6 1.2

簡単に複数変数を表示する

本章では一つの関数で複数の変数を受け取ってそれぞれ表示していく仕組みを作ります.

単一変数を表示する

まずは,全ての基礎となる一つの変数をただ表示する関数を作ります.
これは"関数テンプレート"を用いることで実現できます.

template<class Primitive>
inline void print(const Primitive& x) {
	std::cout << x << "\n";
}

signed main() {
	print(1);
	print(2.5);
	print('c');
}

出力

1
2.5
c

複数変数を受け取って全て表示する

現在は一つの変数しか受け取れず,変数ごとに関数を呼び出す必要があります.
これでは不便なので,"可変引数","再帰関数"を用いて,複数の変数を受け取って左の引数から1つずつ処理を行う関数に書き換えます.

下記のコードではprint関数は"1つ以上"の値を受け取ります.
この際,一番左の引数が'x',残りの引数が'tail'に格納されます.(下記例では x=1,tail=[2.5, 'c'])
そして,関数内で'x'に対する処理を行い,'tail'を引数としてprint関数を呼びます.
すると,'tail'の一番左の値のみが'x'に入り,残りは再び'tail'に格納されます.(下記例では x=2.5,tail=['c'])
この操作の繰り返しにより,複数引数の一番左の変数に対して繰り返し処理を行うことができます.
ただし,このprint関数は引数が0個の場合に対応していないので,引数がないprint関数をオーバーロードする必要があります.

inline void print() {}

template<class Primitive, class... Tail>
inline void print(const Primitive& x, const Tail&... tail) {
	std::cout << x << "\n";
	print(tail...);
}

signed main() {
	print(1, 2.5, 'c');
}

出力

1
2.5
c

コンテナを表示する

今までのprint関数はcoutに直接ぶち込める値のみにしか適用させることが出来ず,コンテナを表示させることはできません.
なので,コンテナ専用のprintContainer関数を新たに作ります.
作り方はprint関数と同じです.

inline void printContainer() {}

template<class Container, class... Tail>
inline void printContainer(const Container& c, const Tail&... tail) {
	for (const auto& x : c) {
		std::cout << x << " ";
	}
	std::cout << "\n";
	printContainer(tail...);
}

signed main() {
	std::vector<int> vec{ 1, 2, 3 };
	std::list<double> lst{ 9.9, 8.8 };
	printContainer(vec, lst);
}

出力

1 2 3
9.9 8.8

print関数とprintContainer関数を統合し,全て同一の関数で表示できるようにする.

前章までの関数ではコンテナの場合はprintContainer関数,そうでない場合はprint関数,というように人力で関数を使い分ける必要がありました.
これでは不便なので,どちらの型でも一つの関数で解決できるようにしていきます.

SFINAE

SFINAEはC++において,テンプレートの展開が上手くいかない場合でもエラーにならないような状況のことです.
SFINAEはテンプレートにより多重定義されている関数を呼び出す際に,いくつかの関数でエラーがでていたとしても,一つエラーのない関数がある場合に起こります.
下記の例では,class Tとしてint型を指定したf関数の呼び出しは,int::fooが存在しないため,本来はエラーとなります.しかし,もう一つの定義では正常に動作するためエラーは発生せずこちらが呼ばれます.
print関数の統合はコンテナに対してSFINAEを誘発させることで実現させます.

struct A{
	typedef int foo;
};

template<class T>
void f(typename T::foo) {
	std::cout << "foo\n";
}

template<class T>
void f(T) {
	std::cout << "no foo\n";
}

signed main() {
	f<A>(0);
	f<int>(0);
}

出力

foo
no foo

コンテナ以外の型へのSFINAE

まず,コンテナかそうでないかはどう見分けるかを考えます.
コンテナには全てに共通したメンバ関数が存在します.
つまりそれらが存在すればコンテナであると言えるでしょう.
今回は拡張for文を使うのにbegin(,end)が必須なため,begin関数が存在すればコンテナであるとして実装を行います.

begin関数のない型に対してSFINAEを作るようにすると以下のようになります.
旧printContainer関数を"返り値の型がc.begin()と同じになるように推論させるprint関数"と定義しなおします.
これにより,begin関数のない型は推論することが出来ずエラーとなるため,SFINAEが発生して既存print関数が呼ばれます.

しかし,これではコンテナの呼び出しが動作しません.
これは,コンテナを引数として場合,定義されているどちらのprint関数にもエラーなく実行できることからオーバーロードの解決ができないためです.

inline void print() {}

template<class Primitive, class... Tail>
inline void print(const Primitive& x, const Tail&... tail) {
	std::cout << x << "\n";
	print(tail...);
}

template<class Container, class... Tail>
inline auto print(const Container& c, const Tail&... tail) -> decltype(c.begin()){
	for (const auto& x : c) {
		std::cout << x << " ";
	}
	std::cout << "\n";
	print(tail...);
	return c.begin();
}

enable_ifによるオーバーロード抑制

enable_ifはbool値と型を受け取り,コンパイル時にbool値がtrueである時に型を定義し,falseであれば型を未定義であるとするテンプレートクラスです.
enable_ifで定義される型をテンプレート関数の戻り値やパラメータの型とすることで,定義されていない場合にSFINAEを起こすことができます.
この仕組みを用いてオーバーロードを抑制し,呼ぶ関数を一つに絞ります.

以下に例を示します.ただし,enable_ifよりも簡潔に記述できるenable_if_tを用いています.
この例ではテンプレート引数としてenable_ifによる型を用いています.
関数の引数の型"T"がint型であるかどうかをstd::is_integralで判定して真偽を判定します.
trueの場合は新たな型が定義され,デフォルトテンプレート引数としてnullptrが代入されます.
この時,もしfalseであれば型が定義されないためエラーとなりSFINAEが発生します.

template <class T, std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr>
void f(T) {
	std::cout << "int\n";
}

template <class T, std::enable_if_t<!std::is_integral<T>::value, std::nullptr_t> = nullptr>
void f(T) {
	std::cout << "not int\n";
}

signed main() {
	f(3);
	f("hello");
}

出力

int
not int

メンバ関数の存在チェック

enable_ifを使うには,型定義するかどうかの真偽値が必要となります.
今回はbegin関数が定義されている場合に型を定義しないようにすればコンテナ型を引数として時のエラーが起こりSFINAEを誘発できます.
しかし,begin関数があるかどうか,つまりメンバ関数の存在チェックをすることができないので自分で実装する必要があります.

実装はSFINAEを利用して行います.
存在判定したいメンバ関数をテンプレート関数の型判定の一部に組み込むことで,
SFINAEを発生させ,存在する時は返り値の型がtrue_type,存在しないときはfalse_typeとなるようにします.
そして,この関数の返り値の型をtrue,falseに変換して返し存在判定が終了となります.

詳しい説明はこちら(codelogy:メンバー関数の存在を確かめる方法)に書かれていますので,参考にしてください.

template<class T>
struct has_begin {
private:
	template <class U>
	static auto check(U x) -> decltype(x.begin(), std::true_type{});
	static std::false_type check(...);
public:
	static bool const value = decltype(check(std::declval<T>()))::value;
};

signed main() {
	std::cout << has_begin<std::list<int>>::value << '\n';
	std::cout << has_begin<int>::value << '\n';
}

出力

1
0

コンテナのオーバーロード制御

必要な知識,クラスが整ったので実際に適用させます.
前の状態ではコンテナのオーバーロード候補が複数あったことにより問題が発生していました
なので,コンテナを使わないprint関数のテンプレート引数として新たにenable_ifによる型を用いて真偽値を反転させることで,begin関数が定義されている型がエラーを出してSFINAEを発生させるようにします.

オーバーロードされたprint関数は互いに行き来することがあるため,プロトタイプ宣言も忘れずにしておきましょう.
これで,関数一つであらゆる値を表示できるようになりました.

inline void print();
template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(const Primitive& x, const Tail&... tail);
template<class Container, class... Tail>
inline auto print(const Container& c, const Tail&... tail) -> decltype(c.begin());

// 前略
template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(const Primitive& x, const Tail&... tail) {
	std::cout << x << "\n";
	print(tail...);
}
//後略

signed main() {
	std::vector<int> vec{ 1, 2, 3 };
	std::list<double> lst{ 9.9, 8.8 };

	print(1, lst, 'c', vec);
}

出力

1
9.9 8.8
c
1 2 3

適用範囲の拡大

あらゆる値を表示できるようになった,と記述しましたが実はまだまだ問題が残っています.
現状残っている問題は以下の3つです.

  • コンテナのコンテナに対応していない
  • string型がコンテナとみなされて出力が分割される
  • mapのようなpairを持つコンテナに対応していない

これらの問題を潰していきます.

コンテナのコンテナを表示

競プロでは2次元配列のような値を持つことが多々あり,コンテナのコンテナの対応していないのは論外です.
対応は簡単で,コンテナの各要素の対して再帰的にprint関数を適用させるだけです.
ただし,コンテナが改行されるようになってしまうため,一部条件式を加えて改行の調整をしました.
これによりコンテナのコンテナのコンテナの...も対応しました.

// 前略
template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(const Primitive& x, const Tail&... tail) {
	std::cout << x << " ";
	if (sizeof...(tail) > 0) {
		std::cout << "\n";
		print(tail...);
	}
}

template<class Container, class... Tail>
inline auto print(const Container& c, const Tail&... tail) -> decltype(c.begin()) {
	for (const auto& x : c) {
		print(x);
	}
	std::cout << "\n";
	print(tail...);
	return c.begin();
}

signed main() {
	std::vector<int> vec{ 1, 2, 3 };
	std::vector<std::vector<int>> mat{ { 5,5 },{ 6,6 } };
	std::vector<std::vector<std::vector<int>>> cut{ { {1,1},{2,2} },{ {3,3},{4,4} } };
	print(vec, mat, cut);
}

出力

1 2 3
5 5
6 6

1 1
2 2

3 3
4 4

mapのようなコンテナを表示

こちらも対応が簡単で,シフト演算子(<<)をオーバーロードしてしまえば終わりです.
オーバーロード関数内でもprint関数を使えば,再帰的なmapなどに対応させることができます.
ただし,受け取ったostreamを無視していたり表示がちょっとずれたりするため,
複雑なコンテナを使うつもりがない場合は,全て"os << .... ;"としてしまう方が良いでしょう.

template<class S, class T>
std::ostream& operator<<(std::ostream& os, const std::pair<S, T>& p) {
	os << "{";
	print(p.first);
	os << ", ";
	print(p.second);
	os << "}";
	return os;
}
// 中略
signed main() {
	std::pair<int, int> p{ 10,100 };
	std::map<int, int> mp{ {1,5},{2,7} };
	std::map<int, std::map<int, int>> mmp{ {1,{{4,4},{6,6}}} ,{2,{{6,8},{7,7}}} };
	print(p, mp, mmp);
}

出力

{10 , 100 }
{1 , 5 } {2 , 7 }
{1 , {4 , 4 } {6 , 6 }
} {2 , {6 , 8 } {7 , 7 }
}

string型を分割せずに表示する

現状では"aeiou"といったstring型文字列を表示すると,"a e i o u"のようにコンテナとしてみなされてスペース区切りで表示されてしまいます.
これでは美しくないので,”aeiou”と繋がって表示できるように改良します.

しかし,begin関数を条件としているためどうやってもstringはコンテナとみなされてしまいます.
そこでテンプレートの特殊化を行います.
テンプレートの特殊化をすると,テンプレート関数において特定のテンプレート引数の場合にのみ適用する関数を定義することができます.
つまりstringの場合のみに呼ばれるstring用print関数を定義することができます.

template<class... Tail>
inline void print(const std::string& s, const Tail&... tail) {
	std::cout << s << "\n";
	print(tail...);
}

signed main() {
	std::string str = "aeiou";
	print(15, str, "kkkkk");
}

出力

15
aeiou
kkkkk

変数名を表示する

実際にデバッグをする時は,値だけ羅列されると逆に分からなくなることがあります.
そこで,"変数名:値"のように分かりやすく表示させるのが一般的かと思われます.
本章では今まで構成したprint関数を拡張し,変数名も同時に表示させるdump関数を作成していきます.

変数名の取得

C++では実はマクロで変数名を文字列として取得することができます.
#define 内で変数名に'#'を付けるとそれがそのまま展開され文字列となります.

注意すべき点として,これは呼び出す直前の変数名を取得しているため,print関数内で変数名を取得してもprint関数内で定義された変数名に置き換わってしまい意味がありません.
以後の節ではこの問題を解決していきます.

#define TO_STRING(args) #args

signed main() {
	int num = 2;
	std::cout << TO_STRING(num) << '\n';
}

出力

num

変数名を維持したまま表示する

print関数を呼んでしまうと変数名が変わってしまうためその前にどうにかする必要があります.
変数名が維持される範囲を考えると,実はその変数のスコープ内,defineの定義内しかありません.
スコープ内は関数を呼ぶ以外出来ないため,必然的にdefine内でどうにかすることが必須となります.
ここまで思考が至ると後は簡単で,define内でstring型として変数名を取って,それを出力してからprint関数を呼べばよいです.

#define dump(arg) \
auto name = #arg; \
std::cout << name << ':'; \
print(arg);

// 中略
signed main() {
	int num = 2;
	dump(num);
}

出力

num:2

複数の変数の変数名を表示する

マクロでは実は可変引数を受け取ることができ,その変数名も取得することができます.
マクロ内では受け取った可変引数は__VA_ARGS__と定義されます.
変数名は#__VA_ARGS__とすることで,全ての変数名を繋げた文字列として展開されます.

#define dump(...) \
auto name = #__VA_ARGS__; \
std::cout << name << '\n'; \
print(__VA_ARGS__);

// 中略
signed main() {
	int num = 2;
	char c = 'a';
	std::vector<int> vec{ 1,2,3 };
	dump(num, c, vec);
}

出力

num, c, vec
2
a
1 2 3

マクロ内の変数名文字列を分割し,print関数内で表示する

現状では最初に変数名の一覧が表示され,次に各値が表示されます.
これでは変数名と値の対応が見づらいため,"変数名:値"といった形になんとかしたいです.
そこで,文字列を','区切りで分割してリスト化し,print関数に渡して内部で左から順に文字列を表示して取り出す,という操作を追加します.
ただし,C++にはsplit関数が存在しないため自作する必要があります.

結果,コンテナにおまけがついてしまいましたが目を瞑ることにします.

#define dump(...) \
auto name = split(#__VA_ARGS__,','); \
print(name, __VA_ARGS__);

inline std::list<std::string> split(std::string str, char del) {
	std::list<std::string> sList;
	std::string s = "";
	for (const auto& c : str) {
		if (c == del) {
			sList.emplace_back(s); s = "";
		} else {
			if (c != ' ' || del == ' ') {
				s += c;
			}
		}
	}
	sList.emplace_back(s);
	return sList;
}
//中略
inline void print(std::list<std::string>& str);
template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(std::list<std::string>& str, const Primitive& x, const Tail&... tail);
template<class Container, class... Tail>
inline auto print(std::list<std::string>& str, const Container& c, const Tail&... tail) -> decltype(c.begin());
template<class... Tail>
inline void print(std::list<std::string>& str, const std::string& s, const Tail&... tail);
//中略
inline void print(std::list<std::string>& str) {}

template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(std::list<std::string>& str, const Primitive& x, const Tail&... tail) {
	std::cout << *str.begin() << ":" << x << " ";
	if (sizeof...(tail) > 0) {
		std::cout << "\n";
		str.pop_front();
		print(str, tail...);
	}
}

template<class Container, class... Tail>
inline auto print(std::list<std::string>& str, const Container& c, const Tail&... tail) -> decltype(c.begin()) {
	std::cout << "-- " << *str.begin() << " --\n";
	str.pop_front(); str.emplace_front("");
	for (const auto& x : c) {
		print(str, x);
	}
	std::cout << "\n";
	str.pop_front();
	print(str, tail...);
	return c.begin();
}

template<class... Tail>
inline void print(std::list<std::string>& str, const std::string& s, const Tail&... tail) {
	std::cout << *str.begin() << ":" << s << "\n";
	str.pop_front();
	print(str, tail...);
}

signed main() {
	int num = 2;
	std::string s = "aeiou";
	std::vector<int> vec{ 1,2,3 };
	dump(num, s, vec);
}

出力

num:2
s:aeiou
-- vec --
:1 :2 :3

調整

大枠が完成したので,微修正を行っていきます.修正点は以下の通りです.

  • マクロ内変数のスコープ制御
  • 変数名追加によるコンテナ表示異常の修正

マクロ内変数のスコープ制御

マクロ内でnameという変数を定義している.
一見このスコープはマクロ内だけに見えるが,マクロはそのまま展開されてしまうため意図せずnameが定義されて名前衝突が起こる可能性がある.
そのため,マクロを全てブロック文で囲んでスコープを制御する.
また,念のため変数名もかぶらないようなものに変えるのが良いだろう.

#define dump(...) \
{ auto __DUMP_NAME_LIST__ = split(#__VA_ARGS__,','); \
print(__DUMP_NAME_LIST__, __VA_ARGS__);}

変数名追加によるコンテナ表示異常の修正

結論から書くと,既存の機能を持ったまま修正することは叶わなかった.
再帰処理を調整することで,コンテナのコンテナまでを限度として正常に表示できるようにはなった.
修正版コードは最終版コードにて示す.

最終版コード

#define dump(...) \
{ auto __DUMP_NAME_LIST__ = split(#__VA_ARGS__,','); \
print(__DUMP_NAME_LIST__, __VA_ARGS__);}

inline std::list<std::string> split(std::string str, char del) {
	std::list<std::string> sList;
	std::string s = "";
	for (const auto& c : str) {
		if (c == del) {
			sList.emplace_back(s); s = "";
		} else {
			if (c != ' ' || del == ' ') {
				s += c;
			}
		}
	}
	sList.emplace_back(s);
	return sList;
}


template<class T>
struct has_begin {
private:
	template <class U>
	static auto check(U x) -> decltype(x.begin(), std::true_type{});
	static std::false_type check(...);
public:
	static bool const value = decltype(check(std::declval<T>()))::value;
};


inline void print(std::list<std::string>& str);
template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(std::list<std::string>& str, const Primitive& x, const Tail&... tail);
template<class Container, class... Tail>
inline auto print(std::list<std::string>& str, const Container& c, const Tail&... tail) -> decltype(c.begin());
template<class... Tail>
inline void print(std::list<std::string>& str, const std::string& s, const Tail&... tail);

template<class S, class T>
std::ostream& operator<<(std::ostream& os, const std::pair<S, T>& p) {
	os << "{" << p.first << ", " << p.second << "}";
	return os;
}


template<class Container>
inline auto printSingle(const Container& c) ->decltype(c.begin()) {
	for (const auto& x : c) {
		std::cout << x << " ";
	}
	std::cout << "\n";
	return c.begin();
}
template<class Primitive, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void printSingle(const Primitive& x) {
	std::cout << x << " ";
}

inline void print(std::list<std::string>& str) {}

template<class Primitive, class... Tail, std::enable_if_t<!has_begin<Primitive>::value, std::nullptr_t> = nullptr>
inline void print(std::list<std::string>& str, const Primitive& x, const Tail&... tail) {
	std::cout << *str.begin() << ":" << x << " ";
	if (sizeof...(tail) > 0) {
		std::cout << "\n";
		str.pop_front();
		print(str, tail...);
	}
}

template<class Container, class... Tail>
inline auto print(std::list<std::string>& str, const Container& c, const Tail&... tail) ->decltype(c.begin()) {
	std::cout << "-- " << *str.begin() << " --\n";
	for (const auto& x : c) {
		printSingle(x);
	}
	std::cout << "\n";
	str.pop_front();
	print(str, tail...);
	return c.begin();
}

template<class... Tail>
inline void print(std::list<std::string>& str, const std::string& s, const Tail&... tail) {
	std::cout << *str.begin() << ":" << s << "\n";
	str.pop_front();
	print(str, tail...);
}

signed main() {
	// 色々な変数
	int num = 2;
	std::string str = "hello";
	std::vector<int> vec = { 1,2,3 };
	std::list<char> lst = { 'a','b','c' };
	std::vector<std::vector<double>> mat = { { 1.1, 2.3 },{ 5.6, 1.2 } };

	// 作成したデバッグマクロ
	dump(num, str, vec, lst, mat);
}
num:2
str:hello
-- vec --
1 2 3
-- lst --
a b c
-- mat --
1.1 2.3
5.6 1.2

終わりに

私が山岡家で一番好きなメニューは「特性味噌ねぎラーメン」です.
山岡家には実はチャーシューが2種類あります.
2種類目のチャーシューは普通のラーメンには入っておらず,白髪ねぎがトッピングされたラーメンを頼んだ時に初めて解禁されます.
さらにやっかいな点としてトッピングとして白髪ねぎを頼んだ場合にはなぜか入らず,券売機で最初からトッピングされている状態のものだけが封入されるのです.
私は特性味噌とプレミアム塩とんこつが好きなのですが,プレ塩の場合デフォルトで白髪ねぎ入りのものが存在しないため,2種類目のチャーシューを食べることを考えると特味噌一択となります.

つまり,数か所改善できる箇所が既に見つかっているので修正できたらいつかまた書きます.