Code Fix

上級

ローカル変数への参照を返すとSegmentation faultになる原因

関数のローカル変数への参照を関数の外に返してしまうバグです。関数を抜けた時点でローカル変数の実体は破棄されるため、返された参照は存在しないメモリを指すことになります。

エラーメッセージの読み方

Segmentation fault
(エラーメッセージなし)
これはプログラム自身の出力ではありません。プログラムは何も言わずに落ちています
Segmentation fault
シェルが検知した異常終了の種類 — OSが「不正なメモリアクセス」などを検出しプロセスを強制終了させました
見つけ方
このメッセージだけでは行番号すら分かりません。原因はコードを読んで特定するしかありません

このエラーが出る典型パターン

パターン1

 1  #include <iostream>
 2  int& getRef() {
 3      int        local = 42;
             ^
 4      return local;
 5  }
 6  int noise() {
 7      int junk[8] = {1,2,3,4,5,6,7,8};
 8      return junk[0];
 9  }
10  int main() {
11      int& r = getRef();
12      noise();
13      std::cout << r << std::endl;
14      return 0;
15  }
Segmentation fault

getRef()が返した参照は、関数を抜けた時点で破棄されたローカル変数localを指したままです。直後に別の関数noise()を呼ぶと、そのスタックフレームが同じメモリ領域を上書きするため、rを読んだ瞬間にクラッシュします。localをstaticにすればプログラム終了まで実体が残り続けるため、参照を安全に返せます。

直し方: int を static int にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  #include <iostream>
 2  int& getValue() {
 3      int        temp = 99;
             ^
 4      return temp;
 5  }
 6  int fill() {
 7      int buffer[8] = {9,9,9,9,9,9,9,9};
 8      return buffer[0];
 9  }
10  int main() {
11      int& v = getValue();
12      fill();
13      std::cout << v << std::endl;
14      return 0;
15  }
Segmentation fault

g++はこの種のミスに対して-Wreturn-local-addrという警告を出しますが、あくまで警告でありコンパイルは止まりません。static を付けてtempの寿命をプログラム終了まで延ばすのが直し方です。

直し方: int を static int にします。

この問題を解いてみる →

パターン3

 1  #include <iostream>
 2  int& makeId() {
 3      int        id = 7;
             ^
 4      return id;
 5  }
 6  int overwrite() {
 7      int stack_data[8] = {0,0,0,0,0,0,0,0};
 8      return stack_data[0];
 9  }
10  int main() {
11      int& id = makeId();
12      overwrite();
13      std::cout << id << std::endl;
14      return 0;
15  }
Segmentation fault

「関数を抜けてもすぐ上書きされなければ値は読める」という考えは危険です。別の関数呼び出しが挟まるだけで、同じスタック領域が再利用され壊れます。ローカル変数をstaticにすれば関数を抜けても実体が破棄されません。

直し方: int を static int にします。

この問題を解いてみる →

パターン4

 1  #include <iostream>
 2  int& getCount() {
 3      int        c = 3;
             ^
 4      return c;
 5  }
 6  int fill2() {
 7      int arr[8] = {1,2,3,4,5,6,7,8};
 8      return arr[0];
 9  }
10  int main() {
11      int& r = getCount();
12      fill2();
13      std::cout << r << std::endl;
14      return 0;
15  }
Segmentation fault

cをstaticにしないと、関数を抜けた時点で無効な参照を返すことになります。

直し方: int を static int にします。

この問題を解いてみる →

パターン5

 1  #include <iostream>
 2  int& getScore() {
 3      int        s = 88;
             ^
 4      return s;
 5  }
 6  int noise2() {
 7      int buf[8] = {0,0,0,0,0,0,0,0};
 8      return buf[0];
 9  }
10  int main() {
11      int& sc = getScore();
12      noise2();
13      std::cout << sc << std::endl;
14      return 0;
15  }
Segmentation fault

別の関数呼び出しを挟むと、同じスタック領域が上書きされクラッシュにつながります。

直し方: int を static int にします。

この問題を解いてみる →

よくある誤解

「関数を抜けてもすぐ上書きされなければ値は読める」という考えは危険です。関数を抜けた直後に別の関数を呼ぶと、その関数のスタックフレームが同じメモリ領域を上書きするため、参照した瞬間にクラッシュすることがあります。g++はこの種のミスに対して -Wreturn-local-addr という警告を出してくれますが、あくまで警告でありコンパイルは止まりません。

実務での勘所

このバグの教訓は「ローカル変数を関数の外に出してはいけない」ではなく、「参照経由で外に出してはいけない」という点です。ローカルの構造体やstd::stringなどを値として(参照を付けずに)そのままreturnするのはまったく安全で、むしろモダンC++はコピー省略(コピーエリジョン、C++17では一部の場面で規格上保証)という最適化により、値渡しのreturnでも余計なコピーが発生しないように処理してくれます。危険なのはあくまで&を付けて参照として外に出す場合に限られます。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)