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;
}
|
[実行結果] ソースファイル ( lst03_28.cpp )
コンストラクタが呼ばれました(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;
}
|
[実行結果] ソースファイル ( lst03_29.cpp )
[関数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;
}
|
[実行結果] ソースファイル ( lst03_30.cpp )
コンストラクタが呼ばれました。: 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 参考文献
最後に、一時オブジェクトや戻り値最適化について、比較的入手しやすい文献を挙げ
ます。
- 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のプログラミング技法」、アジソン・ウェスレイ/アスキー)
(1)の文献では、一時オブジェクトについてごく基本的なことが述べてあり、これは
「第2版」での記述と同じです。ただ「第3版」には、行列のクラスの足し算などで、
「一時オブジェクト」の作成を避けるために補助クラスを導入する方法が述べられてい
ます。
(2)では「一時オブジェクト」や「戻り値最適化」について多くの記述があり(コンパ
イラの振る舞いなど)詳しいが、入門書として読むのには難しいでしょう。(3)では、こ
れらの話題について比較的手際よい解説がなされています。
| 目次 | 前のページ
| 次のページ | ページの先頭 |
Copyright(c) 1999 Yamada, K