openFrameworksをワークショップや授業などで教えていると、ポインタの概念にさしかかった時に突然抽象的になってしまい、挫折する人が出てきてしまう。自分のためのメモも兼ねて、なぜポインタを使うのか、抽象的な解説ではなくopenFrameworksでの分かりやすい実例をベースにして考えてみた。
ダメな例
例えば、以下のように画像をランダムな場所に表示するShowImageという簡単なクラスをつくったとする。
ShowImage.hpp
#pragma once
#include "ofMain.h"
class ShowImage {
public:
void draw();
ofImage image; //表示する画像
float x = ofRandom(ofGetWidth());
float y = ofRandom(ofGetHeight());
float size = 40;
};
ShowImage.cpp
#include "ShowImage.hpp"
void ShowImage::draw(){
//ランダムな場所に指定したサイズで画像を表示
image.draw(x, y, size, size);
}
これを、ofAppから繰り返し生成して配列に格納し、表示していく。画像ファイルの読み込みはofAppで行い、ShowImageクラスに読み込んだ画像を代入していく。
ofApp.h
#pragma once
#include "ofMain.h"
#include "ShowImage.hpp"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
ofImage srcImage; // ソース画像
vector<ShowImage> showImages; //ShowImageの配列
};
ofApp.cpp
#include "ofApp.h"
void ofApp::setup(){
ofBackground(0);
// 画像ファイルを読み込み
srcImage.load("sorceImage.jpg");
}
void ofApp::update(){
ShowImage si; // ShowImageをインスタンス化
si.image = srcImage; //画像を代入
showImages.push_back(si); //配列に追加
}
void ofApp::draw(){
ofSetColor(255);
// ShowImageの配列の数だけ表示
for (int i = 0; i < showImages.size(); i++) {
showImages[i].draw();
}
}
これを実行するとどうなるか。画像のファイルサイズにもよるが、みるみるうちにメモリを消費していき、FPSも極端に落ちていく。実行しているマシンのメモリサイズにもよるが、ずっと起動していると最終的にはアプリごと落ちる。全然ダメダメなプログラムだ。
ポインタで指し示す
では、どのようにすればパフォーマンスが改善するのか? 「ダメな例」の最大の問題は、ShowImageにイメージを代入しているので、ShowImageクラスをインスタンス化して画像を代入する毎に画像ファイルの容量だけメモリを消費している。代入しているということは、つまりは画像のデータをそのままコピーしている状態になっている。
si.image = srcImage; //画像を代入
ここで、ポインタを活用する。ポインタのざっくりしたイメージは、データの実態ではなく、その参照先を「指し示して(Point)」いる機能だ。例えば、先程の例だと巨大な画像データの実態そのものをコピーしてくるのではなく、あそこに画像のデータがあるよと「指し示す」ことができる。
si.image = &srcImage; //画像の参照先を指定
ShowImageから画像を描画する際には、imageは実態ではなくその参照先を指し示すポインタ *image になったので、以下のようにアロー演算子 “->” を使用するように変更する。このアロー演算子の形が、まさに指し示す矢印の形になっている。
image->draw(x, y, w, h);
ざっくりとしたイメージを図示するとこんな感じか?
改良したプログラム
「ダメな例」を修正して、画像像を直接コピーするのではなく、ポインタとして指し示すように変更したプログラムは以下のようになる。
ShowImage.hpp
#pragma once
#include "ofMain.h"
class ShowImage {
public:
void draw();
ofImage *image; //イメージへのポインタ
float x = ofRandom(ofGetWidth());
float y = ofRandom(ofGetHeight());
float size = 40;
};
ShowImage.cpp
#include "ShowImage.hpp"
void ShowImage::draw(){
//ポインタが参照している画像を描画
image->draw(x, y, size, size);
}
ofApp.h
#pragma once
#include "ofMain.h"
#include "ShowImage.hpp"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
ofImage srcImage; //ソース画像
vector<ShowImage> showImages; //ShowImageの配列
};
ofApp.cpp
#include "ofApp.h"
void ofApp::setup(){
ofBackground(0);
// 画像ファイルを読み込み
srcImage.load("srcImage.jpg");
}
void ofApp::update(){
ShowImage si; // ShowImageをインスタンス化
si.image = &srcImage; //画像への参照先を指定
showImages.push_back(si); //配列に追加
}
void ofApp::draw(){
ofSetColor(255);
// ShowImageの配列の数だけ表示
for (int i = 0; i < showImages.size(); i++) {
showImages[i].draw();
}
}
これで、ずっと起動していてもイメージ1個だけのメモリしか消費せず、ShowImageからはそのイメージのデータを参照しているだけなので、安定して稼動するプログラムになるはず! (とはいえ、永久にオブジェクトを複製し続けるので、ずっと起動してると落ちるかも…)
追記: 参照渡し
何人かの方から、この例ではポインタではなく参照をつかったほうが良いのではないかという指摘がありました。どちらが適切なのか、ポインタと参照のどちらをまず覚えるべきなのか、いろいろ難しい議論ですが、とりあえず、ポインタ渡しではなく参照渡しをして描画する例を掲載します。(間違いあればご指摘を)。
ShowImage.hpp
#pragma once
#include "ofMain.h"
class ShowImage {
public:
// draw関数は画像データの参照を受けとる
void draw(ofImage &image);
float x = ofRandom(ofGetWidth());
float y = ofRandom(ofGetHeight());
float size = 40;
};
ShowImage.cpp
#include "ShowImage.hpp"
void ShowImage::draw(ofImage &image){
//参照で渡された画像を描画
image.draw(x, y, size, size);
}
ofApp.h
#pragma once
#include "ofMain.h"
#include "ShowImage.hpp"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
ofImage srcImage; //ソース画像
vector<ShowImage> showImages; //ソース画像を格納する配列
};
ofApp.cpp
#include "ofApp.h"
void ofApp::setup(){
ofBackground(0);
// 画像ファイルを読み込み
srcImage.load("srcImage.jpg");
}
void ofApp::update(){
ShowImage si; // ShowImageをインスタンス化
showImages.push_back(si); //配列に追加
}
void ofApp::draw(){
ofSetColor(255);
// ShowImageの配列の数だけ表示
for (int i = 0; i < showImages.size(); i++) {
//描画の際に画像の参照渡しをする
showImages[i].draw(srcImage);
}
}
たしかに、参照渡しを使うほうが、わかりやすいような気もしてきた… C++は奥深い…
さらに追記: 参照渡しをコンストラクターで行う
上にあげた参照の例、draw()関数の引数として渡してしまっているので、そもそも最初のポインタの例とは別のものになってしまってないか? という疑問をいただきました。うーむ、確かに… そして、別の方から、コンストラクタの引数として参照渡しを受けとるやりかたが、C++的にはいちばんきれいなのではという指摘がありました。openFrameworksのカルチャー(慣習?)とはちょっと異なるのかもしれませんが。
ShowImage.hpp
class ShowImage {
public:
ShowImage(const ofImage &img)
:image(img),
x(ofRandom(ofGetWidth())),
y(ofRandom(ofGetHeight())),
size(40){}
void draw() const;
protected:
const ofImage ℑ
const float x, y, size;
};
inline void ShowImage::draw() const{
image.draw(x, y, size, size);
}
ofApp.h
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
ofImage srcImage;
vector<ShowImage> showImages;
};
ofApp.cpp
void ofApp::setup(){
ofBackground(0);
srcImage.load("wood.jpg");
}
void ofApp::update(){
showImages.push_back(ShowImage(srcImage));
}
void ofApp::draw(){
ofSetColor(255);
for(auto image : showImages){image.draw();}
}