概要
前回のあらすじ
soluna-eureka.hatenablog.com
分割コンパイルでモジュール開発をする場合は,ライブラリのシンボル指向実装とinclude
地獄の回避可能なヘッダ記述手法の都合から,モジュールが異なる(レイヤが1つ下の)自作クラスをメンバ変数に持つなら,実体ではなくポインタで持った方が良く,かつそれらはnew
・delete
を使用してメモリ管理をちゃんと実装し,コンストラクタもデストラクタも確認するべし,的な結論に至った(c++98
縛りゆえスマートポインタ不使用)
しかしモジュールの開発においては,同一レイヤのレベルで自作クラスを実装して,そのまま自作クラスのメンバ変数に組み込む,みたいな場合があるかもしれない(,むしろそれが基本的なクラス設計のやり方なのでは…?)
今回のあらすじ
- 同じモジュール内で実装したクラスでも,ポインタで持つ方が安心できる
- 面倒でもオブジェクトは
new
・delete
する癖をつけた方が良いかもしれない
- そもそもそういうやり方を統一した方が混乱しない,組み込み型じゃないなら尚更
- 実体で持つ場合は必ず
=
の定義を確認するべし
- 例えば,「「クラス
x
」を抱えるクラスy
」をオブジェクトにする場合,クラスy
のコンストラクタが走る前にクラスx
のデフォルトコンストラクタが走る,自作クラスを実体で持つ限りこの挙動は避けられない
- クラス
y
のコンストラクタ内でクラスx
のコンストラクタを叩き,その結果をメンバ変数に=
で接続すると,クラスの初期化と同様に反映できる気もするが,これは初期化の文法ではなく代入の文法として動作する
- なぜなら実体で持つメンバ変数はコンストラクタを叩く前にデフォルトコンストラクタが走って既に実体化したから,その動作はコンストラクタで得たオブジェクトをオブジェクトに
=
で代入することを意味する
- もちろんコピーコンストラクタでもない,例えコピーを名乗ってもコンストラクタは2度と走らない
- つまり演算子
=
が曖昧な自作クラスを実体としてメンバ変数に持つと予期せぬ動作が起きるかも…
- 自作行列ライブラリ(公開できない)の開発中にコレに遭遇して,初めてちゃんと仕様を確認した…
- 1つ下のレイヤのオブジェクトのメンバ変数を叩いて加工するよりも,最初からそのオブジェクトに固有の演算子
=
を定義した方が,実装の責任境界が明確だし後で他のクラスに使われる時も好都合だしで生産性がある
コピー代入演算子のみを扱う,ムーブ代入演算子は扱わない
ただし速度向上を考えたら後者の方が良いのかもしれない…
- 他のクラスに実体で持つ予定のクラス
- 算術演算みたいな使い方をするクラス
- メンバ変数にポインタを持つクラス
new
・delete
またはmalloc
・free
でメモリ確保したポインタを持ってその中でデータを保持している場合,ポインタをコピーするのかポインタの中身をコピーするのかちゃんと考えて実装しよう
実装
.
├── bin
│ └── main
├── libDataBase
│ ├── inc
│ │ └── libDataBase.hpp
│ ├── lib
│ │ └── liblibDataBase.a
│ └── src
│ ├── framework.cpp
│ ├── framework.hpp
│ ├── interface.cpp
│ └── interface.hpp
├── makefile
└── src
└── main.cpp
SHELL = /bin/zsh
MAIN_SRC = src/main.cpp
MAIN_OBJ = main.o
MAIN_BIN = bin/main
GCC_BIN = clang++
GCC_FLG = -std=c++11 -O2
make :
make libDataBase
make main
libDataBase : FORCE
$(call setAllVar,libDataBase)
$(GCC_BIN) $(GCC_FLG) -c $(libDataBase_SRC)
@ar cr $(libDataBase_OBJ) *.o
@rm -f *.o
$(shell echo '#pragma once' > $(libDataBase_REP);)
$(foreach hed,$(libDataBase_HED),$(shell echo '#include "$(hed)"' >> $(libDataBase_REP);))
main : FORCE
$(call setRefVar,libDataBase)
$(GCC_BIN) $(GCC_FLG) -c $(libDataBase_INC) $(MAIN_SRC)
$(GCC_BIN) $(GCC_FLG) -o $(MAIN_BIN) $(MAIN_OBJ) $(libDataBase_LIB)
@rm -f *.o
FORCE :
define setAllVar
$(eval $1_HED = $(notdir $(wildcard $1/src/*.hpp)))
$(eval $1_SRC = $(wildcard $1/src/*.cpp))
$(eval $1_OBJ = $1/lib/lib$1.a)
$(eval $1_REP = $1/inc/$1.hpp)
$(eval $1_INC = -I$1/inc -I$1/src)
$(eval $1_LIB = -L$1/lib -l$1)
endef
define setRefVar
$(eval $1_OBJ = $1/lib/lib$1.a)
$(eval $1_REF = $1/inc/$1.hpp)
$(eval $1_INC = -I$1/inc -I$1/src)
$(eval $1_LIB = -L$1/lib -l$1)
endef
- 適当にデータみたいなクラスを定義する
- コピー代入演算子は
void operator=(const claassName& arg)
でやった
- クラス内の演算子オーバーロードでは左辺が自分自身になるので問題はない
- 引数(のメンバ変数)は変更しないので非変更宣言の
const
をつけておく
- 戻り値を
void
ではなくclassName&
にすると=
を連鎖的に実装できる
a=b=c
を実装するには構文の都合で常に同じクラスを返さないとダメ
- でも今までその書き方をしたことがない,混乱の元だと思っているので
- メンバ関数に非破壊宣言の
const
をつけるときは先頭(戻り値)ではなく末尾
- 戻り値に
const
をつける関数に意味があるのかは正直よくわかっていない
- コピー代入演算子の引数に
const
をつけると,引数arg
を変更しないという宣言になるが,その処理でarg
の持つ関数を呼び出す場合は,それがarg
自身を変更しないことをコード上で保証する必要があるため
- 比較のためにデフォルトコンストラクタ・コピーコンストラクタ・デストラクタの全てでログを吐かせる
#pragma once
#include <string>
namespace database{
class framework{
private:
int ID;
std::string Name;
int Year;
public:
framework();
framework(int id);
framework(const framework &arg);
~framework();
void operator=(const framework &arg);
void setName(std::string name);
void setYear(int year);
int getID() const;
std::string getName() const;
int getYear() const;
};
}
const
に設定したgetXXX()
を使用してconst
で引数を取れるコピー代入演算子を作成する
#include <iostream>
#include "framework.hpp"
using namespace std;
using namespace database;
framework::framework(){
cout << " framework: Default Constructor, ID: " << -1 << endl;
ID = -1;
Name = "";
Year = 0;
}
framework::framework(int id){
cout << " framework: Normal Constructor, ID: " << id << endl;
ID = id;
Name = "";
Year = 0;
}
framework::framework(const framework &arg){
cout << " framework: Copy Constructor, ID: " << ID << endl;
ID = arg.getID();
Name = arg.getName();
Year = arg.getYear();
}
framework::~framework(){
cout << " framework: Destructor, ID: " << ID << endl;
}
void framework::operator=(const framework& arg){
cout << " framework: Substitute, ID: " << ID << " -> " << arg.ID << endl;
ID = arg.getID();
Name = arg.getName();
Year = arg.getYear();
}
void framework::setName(string name){
cout << " Set Name: " << name << endl;
Name = name;
}
void framework::setYear(int year){
cout << " Set Year: " << year << endl;
Year = year;
}
int framework::getID() const{
cout << " Get ID: " << ID << endl;
return ID;
}
string framework::getName() const{
cout << " Get Name: " << Name << endl;
return Name;
}
int framework::getYear() const{
cout << " Get Year: " << Year << endl;
return Year;
}
- 適当にデータベースみたいなクラスを作る
<map>
を使ってみた,あとイテレータを使用せずforeach
を使いたかったので-std=c++11
を指定した
map
自体はint
なid
とframework*
なデータを持つteamData
に使用
addMember
,eraseMember
,showMember
をAPIみたいにして実装
_id_latest
は吐き出したID
の最新の値を指す,ガバガバ実装なのは許して
#pragma once
#include <string>
#include <map>
#include "framework.hpp"
namespace database{
class interface{
private:
framework myData;
std::map<int, framework*> teamData;
int _id_latest;
public:
interface();
interface(std::string name, int year);
~interface();
int addMember(std::string name, int year);
void eraseMember(int id);
void showMember(int id);
};
}
map
のデータ構造がよくわからなかったので,ポインタで持つ処理を採用した
- 比較用に実体で持つオブジェクトも同時に1つ用意した,挙動を確認しよう
- デストラクタのメモリ解放処理を簡単に書きたいがためだけに
c++11
を選んだ
for (const auto& member: map){}
が書けるのはc+11
以降…?
auto
のところはちょっと面倒だったので厳密な書き方をしなかっただけ
#include <iostream>
#include "interface.hpp"
using namespace std;
using namespace database;
interface::interface(){
cout << "interface: Default Constructor" << endl;
}
interface::interface(string name, int year){
cout << "interface: Normal Constructor" << endl;
myData = framework(0);
myData.setName(name);
myData.setYear(year);
_id_latest = 0;
interface::addMember(name, year);
}
interface::~interface(){
cout << "interface: Destructor" << endl;
for (const auto& member: teamData){
delete (member.second);
}
teamData.clear();
}
int interface::addMember(string name, int year){
cout << "Add Team Member" << endl;
++_id_latest;
teamData[_id_latest] = new framework(_id_latest);
teamData[_id_latest]->setName(name);
teamData[_id_latest]->setYear(year);
return _id_latest;
}
void interface::eraseMember(int id){
cout << "Erase Team Member" << endl;
delete (teamData[id]);
teamData.erase(id);
}
void interface::showMember(int id){
cout << "Show Team Member" << endl;
teamData[id]->getID();
teamData[id]->getName();
teamData[id]->getYear();
}
- とても簡単な使用例
- 本当はキー待ちIOとかファイル出力とかちゃんとやるべき
不敬
#include <iostream>
#include <string>
#include "libDataBase.hpp"
using namespace std;
using namespace database;
int main(){
cout << "User name and year" << endl;
string name;
cin >> name;
int year;
cin >> year;
interface mydatabase = interface(name ,year);
int Aiko_id = mydatabase.addMember("敬宮愛子", 2001);
int Hisahito_id = mydatabase.addMember("秋篠宮悠仁", 2006);
mydatabase.showMember(Aiko_id);
mydatabase.showMember(Hisahito_id);
mydatabase.eraseMember(Aiko_id);
mydatabase.eraseMember(Hisahito_id);
};
実行
% bin/main
User name and year
Soluna_Eureka
2000
framework: Default Constructor, ID: -1
interface: Normal Constructor
framework: Normal Constructor, ID: 0
framework: Substitute, ID: -1 -> 0
Get ID: 0
Get Name:
Get Year: 0
framework: Destructor, ID: 0
Set Name: Soluna_Eureka
Set Year: 2000
Add Team Member
framework: Normal Constructor, ID: 1
Set Name: Soluna_Eureka
Set Year: 2000
Add Team Member
framework: Normal Constructor, ID: 2
Set Name: 敬宮愛子
Set Year: 2001
Add Team Member
framework: Normal Constructor, ID: 3
Set Name: 秋篠宮悠仁
Set Year: 2006
Show Team Member
Get ID: 2
Get Name: 敬宮愛子
Get Year: 2001
Show Team Member
Get ID: 3
Get Name: 秋篠宮悠仁
Get Year: 2006
Erase Team Member
framework: Destructor, ID: 2
Erase Team Member
framework: Destructor, ID: 3
interface: Destructor
framework: Destructor, ID: 1
framework: Destructor, ID: 0
見た感じ問題なく動いてるからよし
framework: Default Constructor, ID: -1
interface: Normal Constructor
framework: Normal Constructor, ID: 0
framework: Substitute, ID: -1 -> 0
Get ID: 0
Get Name:
Get Year: 0
framework: Destructor, ID: 0
Set Name: Soluna_Eureka
Set Year: 2000
まずframework
のデフォルトコンストラクタが起爆して,次にinterface
のコンストラクタが起爆する
そして新たにframework
コンストラクタが起爆して,生成されたオブジェクト(右辺値)のコピー代入演算子によるオブジェクト(左辺値としてのメンバ変数)への代入が発生し,最後にデストラクタでオブジェクト(右辺値)が消滅する
Add Team Member
framework: Normal Constructor, ID: 1
Set Name: Soluna_Eureka
Set Year: 2000
Add Team Member
framework: Normal Constructor, ID: 2
Set Name: 敬宮愛子
Set Year: 2001
Add Team Member
framework: Normal Constructor, ID: 3
Set Name: 秋篠宮悠仁
Set Year: 2006
ここではコンストラクタのついでにadd
を使ってmap
のnew framework
に登録している
愛子様,悠仁様,両親王陛下も.わたくしと全く同じ動作をされておりますが,全く同じ関数をご使用賜れておられるゆえ,当然のことだろうと存じ上げます(番号の順番はスルーしてくれ)
Erase Team Member
framework: Destructor, ID: 2
Erase Team Member
framework: Destructor, ID: 3
interface: Destructor
framework: Destructor, ID: 1
framework: Destructor, ID: 0
そしてerase
もデストラクタもちゃんと動いている,撃ち漏らしはないように思える
感想
<map>
と<string>
を初めて使った,しかしmap
はともかくstring
は日本語をそのまま使えてしまっているが,文字コード的に本当にコレで良いのかどうか疑問,ぶっちゃけまだ解決しきった問題でもなさそうだけど
あとポインタで持った方がコピーコスト少ないし拡張性あるし結局はポインタを使う方が良いという結論に至った
自作の行列クラスについて
作ってみたけどコピー代入演算子の引数は参照渡しが使えなかった,だって
z = A*x + B*y
とかやる場合だと,operator*
やoperator+
の返り値は行列クラスそのものになるし,これをそのまんまoperator=
で受け取るには値渡ししかない
参照渡しと値渡しそれぞれのコピー代入演算子は,クラスで実装できてもコードで呼び出すときは曖昧さでエラーを吐くので,ユーザーは使い分けもできない
引数のconst
の厳密な管理も辛い,しかしこっちはちゃんとやるべきだった,マシなコーディング規約を考えて修正したいとは思ってる