3.12.1 コンパイラが暗黙に作成する「一時オブジェクト」
関数がクラス・オブジェクトを値で返すときに、コンパイラは結果を一時的に記憶す るために「一時オブジェクト」を作成します。次のような、クラスXのオブジェクトを 返却する関数 X func(); について、関数が返す値をクラスXのオブジェクトaで受け取るとき a = func(); オブジェクトaの代入演算子が呼び出されます。つまり、 a.operator=( func() ); この代入演算子のプロトタイプは X& X::operator=(const X&); と表されていて、引数にオブジェクト(のconst参照型)を取ります。つまり、関数が 返す結果を受け取るにはオブジェクトが必要です。そのために、コンパイラは「一時オ ブジェクト」を作成し関数が返す値をこれにコピーして、代入演算子に渡します。そし て、代入が終わるとコンパイラはこの「一時オブジェクト」を消滅させます。 同じように、オブジェクトの算術演算 a + b + c; では、 ( a + b ) + c; と解釈されて、最初の足し算(a+b)の結果を一時的に記憶しておく必要があります。 そのために「一時オブジェクト」が作られ、それとcとの足し算が行われます。このとき 演算子+は、非メンバ関数として定義されているときは X operator+(const X&, const X&); で与えられ、あるいはメンバ関数なら X X::operator+(const X&, const X&); と定義されています。いずれにしても、クラスXのオブジェクト(のconst参照)を引 数に取ります。従って足し算(a+b)の結果の値は何らかのオブジェクトに属してい なければなりません。「一時オブジェクト」はその目的で暗黙に生成されます。 この時の振る舞いの詳細は、言語仕様として定められているのではなくコンパイラに 任されていることです。処理系に依存します。コンパイラに要求されてることは、クラ スの一時オブジェクトを導入するときには、その一時オブジェクトに対してコンストラ クタとデストラクタの呼び出しを保証することです。以下では、簡単なクラスを使って その振る舞いを具体的に見てゆきます。
3.12.2 簡単なクラスを使った例
ここでは行列クラスの代わりに、もっと単純なクラスを考えます。このクラスでは、
演算子+、+=を持ち、コンストラクタとデストラクタ、及び代入演算子を持ちます。
class X{
public:
explicit X(int = 0); // コンストラクタ
X(const X&); // コピー・コンストラクタ
~X(); // デストラクタ
X& operator=(const X&); // 代入演算子
X& operator+=(const X&); // 演算子+=
private:
int data;
};
// 演算子+(非メンバ関数)
const X operator+(const X &, const X &);
クラスXは、唯一の内部データ(データメンバ) data を持っていて、コンストラクタ
によって初期設定されます。Xのオブジェクトの足し算は、この内部データの足し算を
意味します。
このクラスのオブジェクトを使った次のコードを考えましょう。
X a(1), b(2); // aの値:1,bの値:2
X c = a + b; // aの値:1,bの値:2,cの値:3
a = b + c; // aの値:5,bの値:2,cの値:3
2行目は、オブジェクトの足し算結果を使ってオブジェクトを初期化します。3行目は、
オブジェクトの足し算結果を代入します。このとき、各オブジェクトの値(データ値)
に注目しておきましょう。これらの値は、コンストラクタやデストラクタがどのオブジ
ェクトから呼ばれたものかを識別する目印として利用します。
次に示すプログラムにあるように、演算子+は演算子+=と関連させて定義していま
す。これは、行列クラスの実装と同じようにしたまでであって、以下の議論では本質的
なことではありません。要は、オブジェクトを値で返す+演算子の振る舞いにあります。
// lst03_27.cpp
// operator+() がオブジェクトを返すときに、コンパイラが
// 暗黙に一時オブジェクトを作成するサンプル・プログラム
#include <iostream.h>
// 演算子+、+=を持つ簡単なクラス
class X{
public:
explicit X(int = 0); // コンストラクタ
X(const X&); // コピー・コンストラクタ
~X(); // デストラクタ
X& operator=(const X&); // 代入演算子
X& operator+=(const X&); // 演算子+=
private:
int data;
};
// コンストラクタ
X::X(int i) : data(i)
{
cout << " コンストラクタが呼ばれました。: data = "
<< data << endl;
}
// コピー・コンストラクタ
X::X(const X &a) : data(a.data)
{
cout << " コピー・コンストラクタが呼ばれました。: data = "
<< data << endl;
}
// デストラクタ
X::~X()
{
cout << " デストラクタが呼ばれました。: data = "
<< data << endl;
}
// 代入演算子
X& X::operator=(const X &right)
{
cout << " 代入演算子= を実行しています。\n";
data = right.data;
return *this;
}
// 演算子+=
X& X::operator+=(const X &right)
{
cout << " 演算子+= を実行しています。\n";
data += right.data;
return *this;
}
// 演算子+
const X operator+(const X &left, const X &right)
{
cout << " [ 演算子+ 内(始め)]\n";
X temp = left; // ローカルオブジェクトを作成
temp += right; // 演算子+=を呼び出す
cout << " [ 演算子+ 内(終了前)]\n";
return temp;
}
int main()
{
X a(1), b(2);
cout << "X c = a + b; を実行前:" << endl;
X c = a + b;
cout << "実行後:" << endl;
cout << " a = b + c; を実行前:" << endl;
a = b + c;
cout << "実行後:" << endl;
return 0;
}
|
コンストラクタが呼ばれました。: data = 1
コンストラクタが呼ばれました。: data = 2
X c = a + b; を実行前:
[ 演算子+ 内(始め)]
コピー・コンストラクタが呼ばれました。: data = 1
演算子+= を実行しています。
[ 演算子+ 内(終了前)]
コピー・コンストラクタが呼ばれました。: data = 3
デストラクタが呼ばれました。: data = 3
実行後:
a = b + c; を実行前:
[ 演算子+ 内(始め)]
コピー・コンストラクタが呼ばれました。: data = 2
演算子+= を実行しています。
[ 演算子+ 内(終了前)]
コピー・コンストラクタが呼ばれました。: data = 5
デストラクタが呼ばれました。: data = 5
代入演算子= を実行しています。
デストラクタが呼ばれました。: data = 5
実行後:
デストラクタが呼ばれました。: data = 3
デストラクタが呼ばれました。: data = 2
デストラクタが呼ばれました。: data = 5
|
実行結果は、Borland C++ Builder 4 に依るものです。2つの文
X c = a + b;
a = b + c;
でコンストラクタとデストラクタの呼び出しの作られ方の違いに注目してください。
最初に、2番目の文から検討しましょう。「a = b + c;」は、
a.operator=( operator+( b, c ) );
と解釈され関数が実行されます。最初に実行される演算子関数operator+()内では、
X temp = b; // ローカルオブジェクトを作成
temp += a; // 演算子+=を呼び出す
の様にローカルオブジェクト temp を生成するときにコピー・コンストラクタが呼ばれ
ます。それからこの temp を返却する際に、コンパイラは「一時オブジェクト」を使っ
て temp のコピーを作ります。関数が戻るときにローカルオブジェクト temp は消滅し
ます。
次に「一時オブジェクト」は、オブジェクトaの代入演算子の引数に渡されて、代入
文「a = b + c;」の末尾で消滅します。従って、2つのデストラクタが呼ばれます。
これに対して、1番目の文「X c = a + b;」では演算子+がオブジェクトの値を返却
する際に「一時オブジェクト」を導入していません。足し算の結果をコピー・コンスト
ラクタでcに直接生成します。2番目に呼ばれるコピー・コンストラクタは、オブジェ
クトcに依るものです。つまり、この場合コンパイラは「一時オブジェクト」の導入を
消去しています。現在の標準的なコンパイラはこの様な振る舞いをします。
これと同じ振る舞いは、関数に値渡しでオブジェクトを渡すときに行われます。次の
関数
void func(X);
に対してクラスXのオブジェクトaを渡すとき
func( a );
現在多くのコンパイラは、関数が受け取るローカルオブジェクトのコピーコンストラク
タを使って直接渡します(§3.3.11 を参照)。
3.12.3 コンパイラ依存
参考に、上の例と違う振る舞いをする古いコンパイラの実行例を示します。以下に示 すのは GNU C++ コンパイラ g++ の古いバージョン( version 2.7.2.1)による ものです(OSは FreeBSD 2.2.8)。
% gcc -v
gcc version 2.7.2.1
% g++ -Wall lst03_27.cpp
% ./a.out
コンストラクタが呼ばれました。: data = 1
コンストラクタが呼ばれました。: data = 2
X c = a + b; を実行前:
[ 演算子+ 内(始め)]
コピー・コンストラクタが呼ばれました。: data = 1
演算子+= を実行しています。
[ 演算子+ 内(終了前)]
コピー・コンストラクタが呼ばれました。: data = 3
デストラクタが呼ばれました。: data = 3
コピー・コンストラクタが呼ばれました。: data = 3
デストラクタが呼ばれました。: data = 3
実行後:
a = b + c; を実行前:
[ 演算子+ 内(始め)]
コピー・コンストラクタが呼ばれました。: data = 2
演算子+= を実行しています。
[ 演算子+ 内(終了前)]
コピー・コンストラクタが呼ばれました。: data = 5
デストラクタが呼ばれました。: data = 5
代入演算子= を実行しています。
デストラクタが呼ばれました。: data = 5
実行後:
デストラクタが呼ばれました。: data = 3
デストラクタが呼ばれました。: data = 2
デストラクタが呼ばれました。: data = 5 |
この場合、オブジェクトを演算結果で初期化するとき、 X c = a + b; 演算子+の結果を「一時オブジェクト」にコピーしておいて、それをcのコピー・コン ストラクタに渡しています。
3.12.4 名無しオブジェクト
普通はオブジェクトには名前がありますが、名前のないオブジェクトを使う場合もあ ります。この“漱石の猫”のようなオブジェクトは、既に度々利用しています。オブジ ェクトを動的にメモリ割り当てする際がそうで、 Matrix *ptr = new Matrix(100,100); と名前の代わりに、ポインタを使います。ここでnew演算子はオブジェクトのメモリ領 域を作成するのにコンストラクタを直接呼び出しています。 オブジェクトを名前で宣言・定義する代わりに、コンストラクタを直接呼び出すこと で名前無しのオブジェクトを生成できます。プログラム例を示します。
// lst03_28.cpp
// 名前無しオブジェクト
// Matrix12.cppと一緒にコンパイルする
#include <iostream.h>
#include "Matrix12.h"
int main()
{
Matrix a = Matrix(5,5);
return 0;
}
|
コンストラクタが呼ばれました(5 行 5 列の行列が作成されました)。 デストラクタが呼ばれました(5 行 5 列の行列を解放します)。 |
このプログラムは、形式上は2つのオブジェクトを生成しています。まず、名前無し
のオブジェクトを生成し、それを使ってオブジェクトaをコピー・コンストラクタで作
成します。
ところが、実行結果はコンストラクタは1つしか呼ばれていません。この例自体は、
利用価値がないように見えます。つまり、このコードは
Matrix a(5,5);
とするのが普通であるからです。しかし、この名無しオブジェクトを関数が返す場合は
どうなるでしょう。次のプログラムを見てください。
// lst03_29.cpp
// 名前無しオブジェクトを返す関数
// Matrix12.cppと一緒にコンパイルする
#include <iostream.h>
#include "Matrix12.h"
Matrix func1()
{
Matrix a(2,3); // ローカルオブジェクト
return a; // 名前のあるオブジェクトを返す
}
Matrix func2()
{
return Matrix(5,6); // 名前無しオブジェクトを返す
}
int main()
{
cout << "[関数1呼び出し前]" << endl;
Matrix a = func1();
cout << "[関数1呼び出し後]" << endl;
cout << endl << "[関数2呼び出し前]" << endl;
Matrix b = func2();
cout << "[関数2呼び出し後]" << endl << endl;
cout << "[main()関数の2つのオブジェクトの破壊]" << endl;
return 0;
}
|
[関数1呼び出し前] コンストラクタが呼ばれました(2 行 3 列の行列が作成されました)。 コピーコンストラクタが呼ばれました(2 行 3 列の行列が作成されました)。 デストラクタが呼ばれました(2 行 3 列の行列を解放します)。 [関数1呼び出し後] [関数2呼び出し前] コンストラクタが呼ばれました(5 行 6 列の行列が作成されました)。 [関数2呼び出し後] [main()関数の2つのオブジェクトの破壊] デストラクタが呼ばれました(5 行 6 列の行列を解放します)。 デストラクタが呼ばれました(2 行 3 列の行列を解放します)。 |
このプログラムでは、1番目の関数は関数内でローカルオブジェクトを作成しそれを 返します。そして2番目の関数では、戻り値のところで名前無しのオブジェクトを作成 しています。 実行結果は、2番目の関数では一時的なオブジェクトは作成されません。中間的なオ ブジェクトの作成を除去しています。つまり、コンパイラにとっては、名前無しのオブ ジェクトは扱いやすく、可能ならば一時的なオブジェクトの導入を除去することが可能 です。
3.12.5 戻り値最適化(一時オブジェクトの除去)
同じやり方を、先ほどの簡単なクラスXに適用してみましょう。
// 演算子+を持つ簡単なクラス
class X{
friend const X operator+(const X&, const X&); // 演算子+
public:
explicit X(int = 0); // コンストラクタ
X(const X&); // コピー・コンストラクタ
~X(); // デストラクタ
X& operator=(const X&); // 代入演算子
private:
int data;
};
このクラスの演算子+を、名前無しオブジェクトを返却するように書き換えます。
// 演算子+(フレンド関数)
const X operator+(const X &left, const X &right)
{
return X(left.data + right.data); // 名無しオブジェクト
}
返却値の名無しオブジェクトを初期化するのに内部表現にアクセスするために、この演
算子関数はフレンド関数にしています。
// lst03_30.cpp
// 戻り値最適化
#include <iostream.h>
// 演算子+を持つ簡単なクラス
class X{
friend const X operator+(const X&, const X&); // 演算子+
public:
explicit X(int = 0); // コンストラクタ
X(const X&); // コピー・コンストラクタ
~X(); // デストラクタ
X& operator=(const X&); // 代入演算子
private:
int data;
};
// コンストラクタ
X::X(int i) : data(i)
{
cout << " コンストラクタが呼ばれました。: data = "
<< data << endl;
}
// コピー・コンストラクタ
X::X(const X &a) : data(a.data)
{
cout << " コピー・コンストラクタが呼ばれました。: data = "
<< data << endl;
}
// デストラクタ
X::~X()
{
cout << " デストラクタが呼ばれました。: data = "
<< data << endl;
}
// 代入演算子
X &X::operator=(const X &right)
{
cout << " 代入演算子= を実行しています。\n";
data = right.data;
return *this;
}
// 演算子+
const X operator+(const X &left, const X &right)
{
cout << " 演算子+ を実行しています。\n";
return X(left.data + right.data); // 名無しオブジェクト
}
int main()
{
X a(1), b(2);
cout << "X c = a + b; を実行前:" << endl;
X c = a + b;
cout << "実行後:" << endl;
cout << " a = b + c; を実行前:" << endl;
a = b + c;
cout << "実行後:" << endl;
return 0;
}
|
コンストラクタが呼ばれました。: data = 1
コンストラクタが呼ばれました。: data = 2
X c = a + b; を実行前:
演算子+ を実行しています。
コンストラクタが呼ばれました。: data = 3
実行後:
a = b + c; を実行前:
演算子+ を実行しています。
コンストラクタが呼ばれました。: data = 5
代入演算子= を実行しています。
デストラクタが呼ばれました。: data = 5
実行後:
デストラクタが呼ばれました。: data = 3
デストラクタが呼ばれました。: data = 2
デストラクタが呼ばれました。: data = 5
|
前のバージョンのプログラム結果と比べて、中間的なオブジェクトの生成が1つ減っ ています。このバージョンでは、コンストラクタとデストラクタのコストが1つ分減り ます。この様に、コンパイラは「一時オブジェクト」の作成を除去することができ、こ れを「戻り値最適化」と呼ぶことがあります。 ここで例示したような簡単な構造のクラスでは、戻り値最適化は巧く機能しました。 それでは、わたしたちのベクトルや行列クラスについてはどうでしょうか。例えば、演 算子+を次のように書き換えてみます。
// ベクトル・クラスの多重定義された+演算子
// 2つのベクトルの和を求め、値渡しで返す
const Vector operator+(const Vector &left, const Vector &right)
{
return Vector(left) += right;// 演算子 += を使う
}
// 行列クラスの多重定義された+演算子
// 2つの行列の和を求め、値渡しで返す
const Matrix operator+(const Matrix &left, const Matrix &right)
{
return Matrix(left) += right; // 演算子 += を使う
}
|
残念ながらこの場合、コンパイラは「一時オブジェクト」を消去してくれません。
return Matrix(left) += right; // 演算子 += を使う
はコンパイラの戻り値最適化の能力を超えて複雑すぎるからです。
3.12.6 参考文献
最後に、一時オブジェクトや戻り値最適化について、比較的入手しやすい文献を挙げ ます。(1)の文献では、一時オブジェクトについてごく基本的なことが述べてあり、これは 「第2版」での記述と同じです。ただ「第3版」には、行列のクラスの足し算などで、 「一時オブジェクト」の作成を避けるために補助クラスを導入する方法が述べられてい ます。 (2)では「一時オブジェクト」や「戻り値最適化」について多くの記述があり(コンパ イラの振る舞いなど)詳しいが、入門書として読むのには難しいでしょう。(3)では、こ れらの話題について比較的手際よい解説がなされています。
- Bjarne Strustrup; The C++ Programming Language (Third Edition), Addison-Wesley (1997). (Bjarne Strustrup著(株)ロングテール,長尾高弘訳「プログラミング 言語C++ 第3版」、アジソン・ウェスレイ/アスキー)
- Stanley B. Lippman; Inside the C++ Object Model,Addison-Wesley (1996). (S.B.リップマン著 三橋二彩子,佐治信之,原田曄訳「C++オブジェクト モデル 内部メカニズムの詳細」、トッパン)
- Scott Meyers; More Effective C++:35 New Ways to Improve Your Programs and Designs, Addison-Wesley (1996). (Scott Meyers著 安村道晃,伊賀聡一郎,飯田朱美訳「More Effective C++ 最新35のプログラミング技法」、アジソン・ウェスレイ/アスキー)
| 目次 | 前のページ | 次のページ | ページの先頭 |
Copyright(c) 1999 Yamada, K