goto文の活用例と代替手段 回避策【多重ループ脱出 リソース解放 関数化回避】

現代のプログラミングでも一部の分野ではgoto文が有効活用されています。今回はそんなgoto文の活用例と代替案を紹介していきます。goto文の有効性への理解にも繋がる内容となっています。

目次

余計な関数化を回避する

リソース解放

多重ループの脱出

余計な関数化を回避する

わざわざ関数化するまでもないような単純な処理は、goto文で簡略化することがありました。

void *ptr = malloc(1);

if (ptr == NULL)
   goto fail;
if (foo(ptr) && bar(ptr))
   goto fail;

do_something(ptr);

fail:
free(ptr);

このように、goto文によって処理の打ち切りを実現できます。

do while false イディオムの活用

do while (false)またはdo while (0)によるイディオムでgoto文を置き換えることができます。

void *ptr = malloc(1);

do {
   if (ptr == NULL)
      break;
   if (foo(ptr) && bar(ptr))
      break;

   do_something(ptr);
} while (0);

free(ptr);

do while文内部のbreak文でgoto文を代替しています。ループ文を用いていますが、処理はきっかり一度だけ実行されます。コンパイラ言語では最適化によってオーバヘッドが回避されることがほとんどです。

無名関数の活用

無名関数で代替することも可能です。以下はC++のラムダ式を活用した例です。return文によって事実上のgoto文の置き換えが実現されます。

void *ptr = malloc(1);

[ptr] {
   if (ptr == NULL)
      return;
   if (foo(ptr) && bar(ptr))
      return;

   do_something(ptr);
}();

free(ptr);

C言語の場合、Clang拡張のブロック(Blocks)を利用することで[ptr]の代わりに^()と書けるようになります。

リソース解放

通常、関数内でメモリ確保を行った場合には、関数を抜ける前にメモリの解放処理が必要になります。以前はgoto文とラベルでそれらの後処理を実現していました。

int fn() {
   int result = 0;
   void *ptr = malloc(1);
   
   if (that(ptr)) {
      result = -1;
      goto fail; // 処理を中断して後処理に向かう
   }
   
   do_something(ptr);

/* 後処理 */
fail:
   free(ptr);
   return result;
}

今でもC言語を用いた開発では、比較的よく使われている作法です。

戻り値の即時返却

後処理が単純な物であれば、戻り値の即時返却テクニックやカンマ演算子の活用が可能です。

int fn() {
   void *ptr = malloc(1);
   
   if (foo()) {
      // 処理の途中で戻り値を返す
      free(ptr);
      return -1;
   }
   
   if (bar())
      return free(ptr), -1; // カンマ演算子で簡略化
   
   do_something(ptr);
   
   free(ptr);
   return 0;
}

defer文の活用

GoやSwift等の近代的なプログラミング言語ではdefer文が利用できます。defer文の実行は処理がスコープを抜ける直前まで延長されます。

let fn = {
   print("a")
   
   defer {
      print("c")
   }
   
   print("b")
}

fn() // "abc"の順に表示される

cleanup属性の活用

cleanupというGCC/Clangの独自拡張を用いることも可能です

改善前

void *ptr = malloc(1);
free(ptr);

改善後

__attribute__((cleanup(free))) void *ptr = malloc(1);

変数宣言されたスコープから処理が抜けると、ptr変数が自動的にfree関数に適用されてfree(ptr)という関数呼び出しが行われます。C言語で事実上のRAIIの実現が可能となります。

なおcleanup属性はポインタ変数に対して適用されるため、以下のような型や関数は作れません。とても残念です。

// 警告 'cleanup' attribute only applies to variables
typedef __attribute__((cleanup(free))) char * GC_string;
GC_string s = malloc(1);
// 警告 'cleanup' attribute only applies to variables
__attribute__((cleanup(free))) void *autorelease(size_t n) { return malloc(n); }
void *p = autorelease(1); // (void *p) != (__cleanup(free) void *p)

C++の活用

C++の場合はRAIIの技法やスマートポインタを活用することが可能です。

RAIIの活用

以下はメモリの確保と解放をクラスのコンストラクタとデストラクタに結びつけるというテクニックです。RAII(Resource Acquisition Is Initialization)の発想に基づいています。

// RAIIを体現したクラス
struct auto_malloc {
   void* ptr;
   auto_malloc(int n) { // コンストラクタ
      ptr = malloc(n);
   }
   ~auto_malloc() {     // デストラクタ
      free(ptr);
   }
};

int fn() {
   // 処理がfn関数を抜けると、
   // 自動的に`p`オブジェクトのデストラクタが呼ばれる
   auto p = auto_malloc(1);
   
   if (foo()) return -1; // `~auto_malloc`呼ばれる
   
   do_something(p.ptr);
   
   return 0;             // `~auto_malloc`呼ばれる
}

スマートポインタの活用

C++の標準ライブラリにはmake_unique等のスマートポインタが予め用意されています。内部的には先程紹介したauto_mallocと同じようなことをしています。

std::unique_ptr<int> ptr = std::make_unique<int>(9);
*ptr += 1;
printf("%d\n", *ptr); // "10\n"

以下のコードと同等の処理が行われます。

int* ptr = new int(9);
*ptr += 1;
printf("%d", *ptr);
delete ptr;

多重ループの脱出

ネストされた複数の繰り返し文から一度に処理を抜け出させたいような場合には、goto文が活用が有効です。このテクニックによって、二重、三重のループ文を一斉に抜け出すことができます。

for (int i = 1; i < 9; i++) {
   for (int j = 1; j < 9; j++) {
      if (i * j == 10) goto label; // 一斉脱出
      printf("%d, ", i * j);
   }
   printf("\n");
}

label:
fflush(stdout);

実行結果

1, 2, 3, 4, 5, 6, 7, 8, 
2, 4, 6, 8,

ラベル付きbreak文の活用

JavaやSwift等のプログラミング言語ではラベル指定が可能な特殊なbreak文を活用できます。

first: for (int i = 1; i < 9; i++) {
   for (int j = 1; j < 9; j++) {
      if (i * j == 10) break first;
      System.out.println(i * j);
   }
   System.out.println();
}

脱出用のフラグの活用

その他の代替案としては、ループ文脱出用のフラグを活用する方法があります。

for (int i = 1; i <= 9; i++) {
   bool escape = false;
   for (int j = 1; j <= 9; j++) {
      if (i * j == 10) {
         escape = true;
         break;
      }
      printf("%d, ", i * j);
   }
   if (escape) break;
   printf("\n");
}

fflush(stdout);

コードが読みづらくなり、また余計な処理コストが生まれるという欠点があります(ただしコンパイラ側の最適化が働く場合もあります)。またループ文のネストが二重、三重と深くなればなるほど、余計な処理とその記述が必要となります。また記述箇所も間違わないように注意しなければなりません。

for (int i = 1; i <= 9; i++) {
   bool escape = false;
   for (int j = 1; j <= 9; j++) {
      for (int k = 1; k <= 9; k++) {
         if (i * j * k == 10) {
            escape = true;
            break;
         }
         printf("%d, ", i * j);
      }
      if (escape) break;
   }
   if (escape) break;
   printf("\n");
}

fflush(stdout);

このようにgoto文を活用していた時よりも複雑なコードになってしまう可能性もあります。これは思わぬバグにつながる可能性もあるため注意が必要です。この手のコード書くくらいなら、むしろgoto文を使ったほうが見通しの良いコードになります。

return文の即時リターンの活用

処理の関数化とreturn文を活用するテクニックもあります。return文呼び出し時に処理が中断されます。関数化の代わりに先程の# 無名関数の活用を活用することも可能です。

void loop() {
   for (int i = 1; i <= 9; i++) {
      for (int j = 1; j <= 9; j++) {
         if (i * j == 10) return; // 関数の処理を中断
         printf("%d, ", i * j);
      }
      printf("\n");
   }
}

int main() {
   loop();
   fflush(stdout);
}

例外を活用

オススメはしませんが、C++の例外機構を活用する斬新なアイディアもあります。

try {
   for (int i = 1; i <= 9; i++) {
      for (int j = 1; j <= 9; j++) {
         if (i * j == 10) throw "goto fail";
         printf("%d, ", i * j);
      }
      printf("\n");
   }
} catch (const char* fail) {}

fflush(stdout);

goto文禁止系コーディング規約への静かなる抗議としてご活用ください。

富豪的プログラミングの活用

処理の内容によってはこのような書き方も可能です。処理コストよりも読みやすさを意識したい場合にオススメです。コンパイラ側の最適化が期待できる場合もあります。

bool sanity = true;
for (int i = 1; i <= 9 && sanity; i++) {
   for (int j = 1; j <= 9 && sanity; j++) {
      if (i * j == 10)
         sanity = false;
      else
         printf("%d\n", i * j);
   }
}
広告