C++のコンストラクタ、デストラクタの呼び出し順序の研究(その1)

C++のコンストラクタ、デストラクタの呼び出し順序は非常に複雑なので、改めて復習・研究を行う。

コンパイラが自動生成する可能性がある特殊メンバ関数

C++のクラスのコンストラクタ等の関数は特にプログラマが記述しなくてもコンパイラによって自動生成される。自動生成される可能性がある特殊メンバ関数は以下の通りである。

名前 シグネチャ
コンストラクタ T()
デストラクタ ~T()
コピーコンストラクタ T(const T&)
コピー代入 T& operator=(const T&)
ムーブコンストラクタ(C++11以降) T(T&&)
ムーブ代入(C++11以降) T& operator=(T&&)

ここではコンストラクタ等がどのような呼び出し順序で行われるかを研究する。

例題

特殊関数が呼ばれた時に出力するクラス

コード(a.h)

#ifndef A_H_
#define A_H_
#include <iostream>
using namespace std;
class A{
public:
  A(){ cout << "A(" << this << ") ctor" << endl; }
  ~A(){ cout << "A(" << this << ") dtor" << endl; }
  A(const A&){ cout << "A(" << this << ") copy ctor" << endl; }
  A& operator=(const A&){ cout << "A(" << this << ") copy asgn" << endl; }
  A(A&&){ cout << "A(" << this << ") move ctor" << endl; }
  A& operator=(A&&){ cout << "A(" << this << ") move asgn" << endl; }
};
#endif

上記のクラスを作れば、どのオブジェクトのどの特殊関数がどのタイミングで呼ばれたかをトレースすることができる。
なお、コンパイル環境はMSYS2 gcc 5.3.0を使い、コマンドラインオプションは "g++ -std=c++11 -O0 " を使用する。

例1:コンストラクタ、デストラクタ

コード

#include "a.h"
int main() {
  A a; // ctor
}      // dtor

実行結果

A(0xffffcc0f) ctor
A(0xffffcc0f) dtor

例2:コピーコンストラクタ

コード

#include "a.h"
int main() {
  A a1;     // a1 ctor
  A a2(a1); // a2 copy
}           // a2/a1 dtor

実行結果

A(0xffffcbff) ctor
A(0xffffcbfe) copy ctor
A(0xffffcbfe) dtor
A(0xffffcbff) dtor

期待通り、"A a2(a1);"の行でコピーコンストラクタが呼ばれている。
なお、コンストラクタが呼ばれる順序はオブジェクトの宣言順で、デストラクタが呼ばれる順序はコンストラクタの順序の逆順のようだ。

例3:変数宣言と同時に代入を行う。

コード

#include "a.h"
int main() {
  A a1;       // a1 ctor
  A a2 = a1;  // a2 copy!!
}             // a2/a1 dtor

実行結果

A(0xffffcbff) ctor
A(0xffffcbfe) copy ctor
A(0xffffcbfe) dtor
A(0xffffcbff) dtor

驚くべきことに、コピー代入ではなく、コピーコンストラクタが呼ばれた。
変数宣言時に代入演算子を使って初期化する場合はコピーコンストラクタが呼ばれるらしい。

例4:変数宣言後に代入を行う。

コード

#include "a.h"
int main() {
  A a1;  // a1 ctor
  A a2;  // a2 ctor
  a2=a1; // a2 asgn
}        // a2/a1 dtor

実行結果

A(0xffffcbff) ctor
A(0xffffcbfe) ctor
A(0xffffcbfe) copy asgn
A(0xffffcbfe) dtor
A(0xffffcbff) dtor

期待通り、コピー代入が呼ばれた。

例5:関数の戻り値を使って初期化する

コード

#include "a.h"
A funcA() {
  cout << "funcA 1" << endl;
  A ret;
  cout << "funcA 2" << endl;
  return ret;
}
int main() {
  A a( funcA() ); // a ctor!!!
} // a dtor

実行結果

funcA 1
A(0xffffcc0f) ctor
funcA 2
A(0xffffcc0f) dtor

奇妙な実行結果となった。出力から考えると、funcA関数の中でmain関数内のローカル変数aのコンストラクタが呼ばれたと考えるのが妥当であると言える。

調べたところ、これはNRVO (Named Return Value Optimization)というコンパイラの最適化が働いた結果で、C++の言語仕様として規定されているらしい。
gccコンパイルオプションに-O0をつけていてもこの最適化機構は働くらしい。

例6:オブジェクト生成後に関数の戻り値を代入

コード

#include "a.h"
A funcA() {
  cout << "funcA 1" << endl;
  A ret;
  cout << "funcA 2" << endl;
  return ret;
}
int main() {
  A a; // a ctor
  a = funcA(); // ret ctor, a move, ret dtor
} // a dtor

実行結果

A(0xffffcbfe) ctor
funcA 1
A(0xffffcbff) ctor
funcA 2
A(0xffffcbfe) move asgn
A(0xffffcbff) dtor
A(0xffffcbfe) dtor

まず、"A a;" によってaのコンストラクタが呼ばれ、funcA関数内でretのコンストラクタが呼ばれる。
次に代入によってaのムーブ代入が呼ばれる。ムーブ代入後、retのデストラクタが呼ばれる。
最後に、aのデストラクタが呼ばれる。

つまりこういうことだと考えられる。
プログラマがムーブ演算やデストラクタを記述する際は、デストラクタ内ではムーブ演算によって所有権が他オブジェクトに移されたメンバ変数がある可能性も考慮しながら破棄しなければならない。
たとえば、ムーブ後はリソースを指すポインタはnullptrにしておき、デストラクタ内で誤って破棄されることがないようにする必要があるということだろう。

MNISTのデータファイルをC言語で読み込む

動機

私は現在「ゼロから作るDeep Learning」を読み進めています。しかし、この本ではPythonが使用されています。自分の理解のために、C言語、もしくはC++でこのディープラーニングを実装していきたいというのが動機です。

MNISTとは

機械学習の分野でよく用いられている数字の画像のデータセットです。
以下のページで公開されています。
MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

MNISTのデータ書式

上記ページの "FILE FORMATS FOR THE MNIST DATABASE" で説明されています。
それによると、以下のような書式です。(3次元データの場合)

  • 最初の2バイト:マジックナンバー 0x0000
  • 次の1バイト:1要素のバイト数の情報。0x8なら1要素=1バイト
  • 次の1バイト:次元数。3なら3次元。
  • 次の4バイト:次元0のサイズ
  • 次の4バイト:次元1のサイズ
  • 次の4バイト:次元2のサイズ
  • 残り:データ(次元0×次元1×次元2のバイト数)

Stirlingなどのバイナリエディタで見ると、確かにそのような書式になっています。
f:id:kenjiwn:20170709200226p:plain

また、"Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black)." と記載されています。
これは通常の画像形式とは逆なのでこれに従うのであれば反転が必要です。

これらを踏まえて、MNISTの画像をPGM形式に変換するプログラムをC言語で記述してみました。

github.com

出力されたPGMファイルをIrfanViewなどのビュアーで見ると、確かに数字っぽい画像が出現してきます。
f:id:kenjiwn:20170709201139p:plain

ついでに、ラベルデータをテキストで出すプログラムも作りました。

github.com

実行すると以下のような出力がされており、画像とラベルデータが一致していることが目視で確認できます。

50419213143536172869
40911243273869056076
18793985933074980941
44604561001716302117
90267839046746807831
57171163029311049200
20271864163459133854
77428586734619960372
82944649709295159123
23591762822507497832
11836103100172730465
26471899307102035465
86375809103122336475
06279859211445641253
93905965741340480436
87609757211689415229
03967203543658954742
73489192879187413110
23949216847744925724
42197287692238165110