AP4 - 付録4.ループの裏技repマクロ 解説 /

実行時間制限: 0 msec / メモリ制限: 0 KB

「1.10.while文」に戻る
「1.11.for文・break・continue」に戻る

キーポイント

  • repマクロを使うと「指定した回数だけ処理を繰り返す」ことができる
  • repマクロを使うには宣言が必要
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
  • 指定した回数だけ処理を繰り返す
rep (i, 回数) {
  処理
}
  • カウンタ変数iを使えば「何回目の繰り返しか」という情報が利用できる
  • カウンタ変数は0から始まり「回数-1」で終わる
  • カウンタ変数名は変更できる
  • breakを使うとループを途中で抜けられる
  • continueを使うと後の処理を飛ばして次のループへ行ける

ループ構文の裏技

この付録では、ループ構文を楽に書くための裏技であるrepマクロについて説明します。
while文やfor文がよくわからなかった人でも、シンプルなループ(繰り返し)の機能であるrepマクロなら理解できるかもしれません。

この付録を読めば1.10.while文1.11.for文・break・continueを読まなくても、練習問題のEX10.棒グラフの出力EX11.電卓を作ろうを解くことができます。
ただし、1.12以降の説明ではrepマクロではなくfor文を使って説明するので、EX10とEX11を解き終わったら1.10と1.11も読むようにしましょう。この付録を読む前より簡単に理解できるはずです。

repマクロの是非

この部分はプログラミング経験者向けの文です。経験者の方はクリックで説明を開いて読んでください。

クリックで説明を開く プログラミング経験者の中にはrepマクロに否定的な人もいると思います。ここではrepマクロをAPG4bで紹介する理由について説明します。

C++14時点では標準で「N回の繰り返し処理」を簡潔に書く構文が用意されていません。その処理には通常for文を用いますが、for文は初心者にとっては2点大きな問題があります。

1点目として、for文1行で複数の処理を行っているため理解しづらいという点があげられます。本来ループ構文で理解してほしいことは「処理を繰り返す」ということですが、for文は構文の動きを考えざるを得ないため、重要でないことに意識を割かれて理解が妨げられてしまいます。

2点目として、カウンタ変数を書き間違えるバグにハマって時間を浪費してしまう点があげられます。for文はカウンタ変数を書く場所が3箇所あるので、書き間違えるリスクが大きいです。1重のループにおいてはコンパイルエラーが出るだけなのでまだましですが、多重ループの場合はijを書き間違えることによって非常に発見し辛いバグになり得ます。for文のカウンタ変数の書き間違えによるバグは熟練者でも発生しうるバグであり、完全に防ぐことは難しいです。この問題は学習者のモチベーションを削ぎ、挫折するきっかけになりかねません。(コードスニペットとIDE等のリファクタ機能を用いることを徹底したり、静的コード解析ツールを使いこなせばある程度防ぐことはできますが、それでも完全ではありませんし、ツールの理解や環境構築等も必要になります。)

これらの問題はrepマクロを使うことによってほぼ解決されます。このようにrepマクロは初心者がC++を学ぶ場面において性質が良いため、APG4bでは紹介しています。

それでもマクロによる構文拡張は行うべきではないという方もいるかもしれません。しかし、マクロによる構文拡張の問題は個人で小規模なプログラムを書く分には基本的に発生しませんし、それがrepマクロひとつだけであるならなおさらです。この付録ではrepマクロが標準の記法でないことも説明しているので、しっかりと読んでいれば学習者が勘違いすることもありません。
また、実はマクロによるループ構文の拡張はLinuxカーネルのソースコードでもfor_each_...等の名前でよく利用されています。LinuxカーネルのソースコードはC言語ですし、他にも様々な事情の違いはありますが、for文の扱いづらさが広く認識されている例だと言えるでしょう。
これを期にrepマクロを利用しても良い状況、または利用するべき状況について考え直してみてはいかがでしょうか。

repマクロ

repマクロは「指定した回数だけ処理を繰り返す」という機能です。
次のプログラムは「"Hello"と出力して改行した後、"AtCoder"と出力する処理」を3回繰り返します。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  rep(i, 3) {
    cout << "Hello" << endl;
    cout << "AtCoder" << endl;
  }
}
実行結果
Hello
AtCoder
Hello
AtCoder
Hello
AtCoder

repマクロの準備

repマクロを使うには、今まで使っていたプログラムの基本形を次のように変える必要があります。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {

}

3行目に#define rep(i, n) for (int i = 0; i < (int)(n); i++)と追記しています。この部分の詳細は第4章で説明します。今は理解していなくても良いので、コピー&ペーストして使いましょう。

repマクロの使い方

repマクロは基本的に次のように書き、指定した回数だけ処理を繰り返します。

rep(i, 回数) {
  処理
}

はじめのプログラムでは「回数」に3、「処理」にcout << "Hello" << endl; cout << "AtCoder" << endl;を指定しています。

rep(i, 3) {
  cout << "Hello" << endl;
  cout << "AtCoder" << endl;
}

「回数」の部分には3のように直接値を書いても良いですし、変数や計算処理を書いても良いです。

カウンタ変数

変数iを利用することで「何回目の繰り返しか」という情報を使うことができます。
次のプログラムは「"Hello rep: (何回目の繰り返しか)"と出力する処理」を3回繰り返すものです。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  rep(i, 3) {
    cout << "Hello rep: " << i << endl;
  }
}
実行結果
Hello rep: 0
Hello rep: 1
Hello rep: 2

i0から始まることに注意してください。このように、プログラム中では番号を数えるときに1ではなく0から始めることが多いです。
0から始めたほうがプログラムをシンプルに書けることが多いためそうなっています。最初はわかりにくく感じるかもしれませんが、慣れるようにしましょう。

iのように「何回目の繰り返しか」を管理する変数のことをカウンタ変数と呼ぶことがあります。repマクロにおいては、カウンタ変数の型はint型です。

カウンタ変数名の変更

次の例ではiの代わりにatcoderをカウンタ変数にしています。

rep(atcoder, 3) {
    cout << "Hello rep: " << atcoder << endl;
}

rep()の最初の部分でカウンタ変数名を指定します。カウンタ変数も他の変数と同じルールで名前を指定できます。

カウンタ変数も考慮すると、より正確にはrepマクロの記法は次のようになります。

rep(カウンタ変数名, 回数) {
  処理
}

カウンタ変数名には基本的にiを使い、iが使えない場合はj, k, l...と名前をつけていくのが一般的です。

応用例

N人の合計点を求めるプログラムを作ってみましょう。
次のプログラムは「入力の個数N」と「点数を表すN個の整数」を入力で受け取り、点数の合計を出力します。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)  

int main() {
  int N;
  cin >> N;

  int sum = 0; // 合計点を表す変数
  int x;       // 入力を受け取る変数

  rep(i, N) {
    cin >> x;
    sum += x;
  }

  cout << sum << endl;
}
入力
3
1 10 100
実行結果
111

合計点を表す変数sumを作っておき、ループするたびに入力を変数xに入れ、sumに足しています。

このプログラムが実行される様子を表したのが次のスライドです。

このように、繰り返し処理を使えば様々な処理が行えるようになります。
ループ構文を使って様々な処理を書く方法は2.01.ループの書き方2.02.多重ループで詳しく説明します。


breakとcontinue

repマクロのループを制御する命令として、breakcontinueがあります。

break

breakはループを途中で抜けるための命令です。

breakのいらすと

breakを使ったプログラムの例です。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {

  // breakがなければこのループは i == 4 まで繰り返す
  rep(i, 5) {

    if (i == 3) {
      cout << "ぬける" << endl;
      break; // i == 3 の時点でループから抜ける
    }

    cout << i << endl;
  }

  cout << "終了" << endl;
}
実行結果
0
1
2
ぬける
終了

if文で i == 3 が真になったとき、break;の命令を実行することでループを抜け、終了と出力しています。

continue

continueは後の処理をとばして次のループへ行くための命令です。

continueのいらすと

continueを使ったプログラムの例です。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {

  rep(i, 5) {

    if (i == 3) {
      cout << "とばす" << endl;
      continue; // i == 3 のとき これより後の処理をとばす
    }

    cout << i << endl;
  }

  cout << "終了" << endl;
}
実行結果
0
1
2
とばす
4
終了

上のプログラムでは、if文で i == 3 が真になったとき、continue;の命令を実行することでcontinueより下の部分を飛ばし、次のループに入ります。


注意点

repマクロを使用するときの注意

repマクロは簡潔でわかりやすい構文ですが、多人数が参加する開発等では利用できない場合もあります。
実はrepマクロはC++の標準の構文ではなく、マクロという機能を使って自分で定義した構文です。

多人数開発でマクロにより様々な構文が定義されるとプログラムが過度に複雑になるなどの理由から、マクロによる構文の定義は禁止されていることがよくあります。 repマクロを使用するときはコーディングルールを確認するようにしましょう。

ただし、競技プログラミングやAPG4bでrepマクロを使う分には何も問題は無いので、気にせず使ってください。個人開発でも基本的には使って良いです。
なお、この付録を除くAPG4bの説明文ではrepマクロは使わずにwhile文やfor文を使いますが、練習問題を解く時はrepマクロを利用しても構いません。

repマクロの定義し忘れ

#define rep(i, n) for (int i = 0; i < (int)(n); i++)と書くことを忘れると次のようなコンパイルエラーが発生します。

#include <bits/stdc++.h>
using namespace std;

int main() {
  rep(i, 3) {
    cout << "Hello" << endl;
    cout << "AtCoder" << endl;
  }
}
コンパイルエラー
./Main.cpp: In function ‘int main()’:
./Main.cpp:5:7: error: ‘i’ was not declared in this scope
   rep(i, 3) {
       ^
./Main.cpp:5:11: error: ‘rep’ was not declared in this scope
   rep(i, 3) {
           ^

細かい話

細かい話なので、飛ばして問題を解いても良いです。

repマクロの派生

もう少し自由度を上げて、「カウンタ変数がSからN-1の間処理を繰り返す」というrepマクロを利用することもあります。
次のプログラムは「Hello rep2: (カウンタ変数の値)と出力する処理」をカウンタ変数が2~4の間繰り返します。

#include <bits/stdc++.h>
using namespace std;
#define rep2(i, s, n) for (int i = (s); i < (int)(n); i++)

int main() {

  rep2(i, 2, 5) {
    cout << "Hello rep2: " << i << endl;
  }

}

実行結果

Hello rep2: 2
Hello rep2: 3
Hello rep2: 4

このマクロを利用するには#define rep2(i, s, n) for (int i = (s); i < (int)(n); i++)とはじめの部分に追記する必要があります。

なお、次のようにすれば通常のrepマクロで代用することも可能です。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
#define rep2(i, s, n) for (int i = (s); i < (int)(n); i++)

int main() {

  int s = 2, n = 5;

  rep2(i, s, n) {
    cout << "Hello rep2: " << i << endl;
  }

  // 上のループとほとんど同じ処理
  rep(ii, n - s) {
    int i = ii + s;
    cout << "Hello rep: " << i << endl;
  }
}

実行結果

Hello rep2: 2
Hello rep2: 3
Hello rep2: 4
Hello rep: 2
Hello rep: 3
Hello rep: 4

カウンタ変数のスコープ

repマクロのカウンタ変数はrepマクロ{ }中でしか使えません。
次の例では、repマクロのカウンタ変数であるiがスコープの範囲外で使われているため、コンパイルエラーが発生しています。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  rep(i, 3) {
    cout << "Hello!" << endl;
  }
  cout << i << endl;
}
コンパイルエラー
In function ‘int main()’:
9:11: error: ‘i’ was not declared in this scope
   cout << i << endl;
           ^

「変数のスコープ」で見たエラーと同じく、‘i’ was not declared in this scope('i'はこのスコープで宣言されていません)というエラーが出ています。

{ }の省略

if文と同様に、repマクロの処理が一文のみの場合も{ }を省略できます。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {

  rep(i, 3)
    cout << "Hello!" << endl;

}

repマクロのネスト

if文と同様に、repマクロもネストさせることができます。そのような書き方は多重ループと呼ばれます。
詳しくは2.02.多重ループで説明します。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {

  rep(i, 2) {
    rep(j, 2) {
      cout << "i: " << i << ", j:" << j << endl;
    }
  }

}
実行結果
i:0, j:0
i:0, j:1
i:1, j:0
i:1, j:1

forループとの関係

repマクロはfor文の機能を制限する代わりにシンプルに書けるようにしたものです。
以下のrepマクロとfor文はほとんど同じ意味になります。

int n = 5;

rep(i, n) {
  処理
}

for (int i = 0; i < n; i++) {
  処理
}

途中でループ回数やカウンタ変数を書き換えた場合

repマクロの処理の中でループ回数やカウンタ変数を書き換えると、ループの処理に影響が出ます。
次の例ではその両方をループの中で書き換えています。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  int n = 4;
  rep(i, n) {
    cout << i << ", " << n << endl;
    n--; // 回数を書き換え
    i++; // カウンタ変数を書き換え
  }
}
実行結果
0, 4
2, 3

このように、ループ回数やカウンタ変数を書き換えるとループ回数が変わります。
なぜこのような挙動になるかは上で説明した「forループとの関係」を頭に入れた上で、1.11.for文・break・continueを読めば理解できます。
よくわからないうちは、ループ回数を保持する変数とカウンタ変数は書き換えないほうが無難です。


問題

以下の2つの問題は1.10.while文1.11.for文・break・continueの練習問題ですが、repマクロを使って解くとことができます。
ヒントと解答例のrepマクロ版を用意したので、挑戦してみてください。

EX10.棒グラフの出力

ヒント

クリックでヒントプログラムを見る

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  cout << "A:";
  int i = 0;
  rep(i, 5) {
    cout << "]";
  }
  cout << endl;
}

ヒント出力

A:]]]]]

解答例

クリックで解答例を見る

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  int A, B;
  cin >> A >> B;

  cout << "A:";
  rep(i, A) {
    cout << "]";
  }
  cout << endl;

  cout << "B:";
  rep(i, B) {
    cout << "]";
  }
  cout << endl;
}

EX11.電卓を作ろう

ヒント1

クリックでヒントプログラムを見る

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  int sum = 0;

  rep(i, 5) {
    int x;
    cin >> x;

    sum += x;

    cout << i + 1 << ":" << sum << endl;
  }

  cout << sum << endl;
}

解答例

クリックで解答例を見る

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)

int main() {
  int N, A;
  cin >> N >> A;

  rep(i, N) {
    int x;
    string op;
    cin >> op >> x;

    if (op == "+") {
      A += x;
    }
    else if (op == "-") {
      A -= x;
    }
    else if (op == "*") {
      A *= x;
    }
    else if (op == "/" && x != 0) {
      A /= x;
    }
    else {
      cout << "error" << endl;
      break;
    }

    cout << i + 1 << ":" << A << endl;
  }

}


「1.10.while文」に戻る
「1.11.for文・break・continue」に戻る