デバッグ用の可変長引数関数を作ってみた


目標の形

int main(){
    int abc=10;
    int hoge=5;
    string str="aiueo";
    debug(abc,hoge,str);
}
理想の出力結果
abc:10 hoge:5 str:aiueo

debugはマクロでも関数でもなんでも良いから変数名とその値を空白区切りで出力してほしかった(できれば標準エラー出力)

結論

#define debug(...) debug_func(0, #__VA_ARGS__, __VA_ARGS__)

template <typename T>
void debug_func(int i, T name) {
  cerr << endl;
}

template <typename T1, typename T2, typename... T3>
void debug_func(int i, const T1 &name, const T2 &a, const T3 &...b) {
  for ( ; name[i] != ',' && name[i] != '\0'; i++ ) cerr << name[i];
  cerr << ":" << a << " ";
  debug_func(i + 1, name, b...);
}

2つの関数とマクロを定義することで実現できた

改行の手前に余分な空白が入るのはまあデバッグ用だし...
\nでなくてendlにしたのも甘え

追記:本ページ下部に修正を行い、改行前に空白が入らないコードが載せてあります。


それぞれの説明

まずは

#define debug(...) debug_func(0, #__VA_ARGS__, __VA_ARGS__)

から

これは可変長引数をマクロで読み取っている

なんでマクロなのかというと変数名を表示するのに#演算子が必要だったから

#演算子で変数名を文字列として取得できる

例として、debug(a,bb,ccc)と呼び出すと、"a,bb,ccc"という文字列が取得できるので変数名を出力するときは,を読み飛ばす必要がある


次に

template <typename T>
void debug_func(int cnt, T name) {
    cerr << endl;
}

これはdebug_funcの終了のための関数

最後の関数と名前は同じだけど引数が違う

再帰的に呼び出しながら出力していって、最後に改行するという流れ


最後に

template <typename T1, typename T2, typename... T3>
void debug_func(int i, const T1 &name, const T2 &a, const T3 &...b) {
  for ( ; name[i] != ',' && name[i] != '\0'; i++ ) cerr << name[i];
  cerr << ":" << a << " ";
  debug_func(i + 1, name, b...);
}

この関数を見た人は全員次のような疑問ですえ!?ってなる(と思う)

Q.#define debug(...) debug_func(0, #__VA_ARGS__, __VA_ARGS__)だと引数が3つなのにこれは引数が4つだから間違ってるんじゃないか?

A.間違ってないです


実は可変長引数を関数の引数に取るときは...(pack演算子)を使える

pack演算子は前につけるか後につけるかで変わってくる

  • 前につける

    int ...b

    可変長引数を一つの変数に受け取る

  • 後につける

    b...

    変数から可変長引数を取り出す

可変長引数の内、一つを変数に入れて残りをpack演算子でもう一つの変数にまとめているイメージ

int int_sum() {
    return 0;
}

template <typename T1, typename... T2>
int int_sum(const T1 &a, const T2&... b){
    return a + int_sum(b...);
}

int main(){
    cout<<int_sum(10,20,5)<<endl;
}
// 結果
35

また、下記のように#define sum(...) int_sum(__VA_ARGS__)のようにして呼び出しても良い

#define sum(...) int_sum(__VA_ARGS__)

int int_sum() {
    return 0;
}

template <typename T1, typename... T2>
int int_sum(const T1 &a, const T2&... b){
    return a + int_sum(b...);
}

int main(){
    cout<<sum(10,20,5)<<endl;
}


余談
for ( ; name[i] != ',' && name[i] != '\0'; i++ ) cerr << name[i];

↑この部分は変数名が○○,○○○,....という風にコンマ区切りで渡されるのでコンマまで一文字ずつ出力している

nameの型を調べてみるとconst char*だったので最後に\0がある

nameの型を調査するコード
#include <iostream>
#include <typeinfo>
using namespace std;

#define debug(...) debug_func(0, #__VA_ARGS__, __VA_ARGS__)

template <typename T>
void debug_func(int i, T name) {
  cerr << endl;
  cerr << "typename:" << typeid(name).name() << endl;
  cerr << "const char* same?:" << boolalpha << (typeid(name) == typeid(const char *)) << endl;
}

template <typename T1, typename T2, typename... T3>
void debug_func(int i, const T1 &name, const T2 &a, const T3 &...b) {
  for ( ; name[i] != ',' && name[i] != '\0'; i++ ) cerr << name[i];
  cerr << ":" << a << " ";
  debug_func(i + 1, name, b...);
}

int main() {
  int hoge = 10;
  int foo = 101391;
  long long hofoo = 101010;
  debug(hoge, foo, hofoo);
}
出力結果
hoge:10  foo:101391  hofoo:101010 
typename:PKc
const char* same?:true

typenamePKcとなっているのはPがポインタ、Kconstccharを表していて、mangled nameと呼ばれているらしい


追記

yumetodoさんからコメントをいただきましたので、勝手ながら記事に追記いたしました。

#include <iostream>
#include <string_view>
#define debug(...) debug_func(#__VA_ARGS__, __VA_ARGS__)
template <typename T, typename... Rest>
void debug_func(std::string_view name, const T& a, Rest&&... rest)
{
  const auto end = name.find_first_of(',');
  std::cerr << name.substr(0, end) << ":" << a;
  if constexpr (sizeof...(rest) == 0) {
    std::cerr << std::endl;
  }
  else {
    std::cerr << ' ';
    debug_func(name.substr(name.find_first_not_of(' ', end + 1)), std::forward<Rest>(rest)...);
  }
}

int main()
{
  int a = 3;
  double b = 4.2;
  debug(a, b);
  debug(b, a);
}


私が書いたコードでは、debug(std::min(3,7));といった,を含むものを出力しようとした場合にstd::min(3:3というような想定していない出力になってしまうことが確認できたので修正いたしました
具体的には、(){}[]<>で囲まれた,であればスルーするようなプログラムになっています
(最適な解決策とは思えませんが...)

修正版
#include <iostream>
#include <stack>
#include <string_view>
using namespace std;

#define debug(...) debug_func(#__VA_ARGS__, __VA_ARGS__)

template <typename T, typename... Rest>
void debug_func(string_view name, const T &a, Rest &&...rest) {
  stack<char> brackets;
  string_view left_brackets = "({[<";
  string_view right_brackets = ")}]>";
  int end = name.size();
  for ( int i = 0; i < (int)name.size(); i++ ) {
    if ( left_brackets.find(name[i]) != string::npos ) {
      brackets.push(name[i]);
    }
    if ( right_brackets.find(name[i]) != string::npos ) {
      if ( !brackets.empty() ) brackets.pop();
    }
    if ( name[i] == ',' && brackets.empty() ) {
      end = i;
      break;
    }
  }
  cerr << name.substr(0, end) << ":" << a;
  if constexpr ( sizeof...(rest) == 0 ) {
    cerr << endl;
  } else {
    cerr << ' ';
    debug_func(name.substr(name.find_first_not_of(' ', end + 1)), forward<Rest>(rest)...);
  }
}

int main() {
  int aaa = 10;
  debug(10, aaa, max(10, 4), min(10, 1));
}


参考

詳しくはこちらをご覧ください