blog

C++プライマー学習ノート第13章

合成コピーコンストラクタは、このクラス型のオブジェクトのコピーを防ぐために使用され、引数のメンバを1つずつ生成されるオブジェクトにコピーします。配列型の場合、合成コンストラクタは配列型のメンバを要素ご...

Sep 22, 2020 · 8 min. read
シェア

コピー制御

コピー代入と破棄

コピーコンストラクタ

  • 最初の引数が自身の型への参照で、追加の引数がデフォルト値を持つコンストラクタ。
class Foo{
public:
 Foo();// 
 Foo(const Foo&);//コピー ほとんど常に const
}
  • 合成コピーコンストラクタは、そのクラス型のオブジェクトをコピーしないようにするために使用され、その引数のメンバを要素ごとに生成されるオブジェクトにコピーします。配列型の場合、合成コンストラクタは配列型のメンバを要素ごとにコピーします。
class Sales_data{
public:
 Sales_data(const Sales_data&);
private:
 std::string bookNo;
 int units_sold = 0;
 double revenue = 0.0;
};
Sales_data:Sales_data(const Sales_data& orig):bookNo(orig.bookNo),units_sold(orig.units_sold),revenue(orig.revenue){}
  • コピーの初期化が発生するタイミングと、コピーの初期化がコピー・コンストラクタに依存するのか、移動コンストラクタに依存するのか。どのような場合に初期化が行われるのか: =、実パラメータが非参照型の形式パラメータに渡される場合、非参照の戻り値を持つ関数がオブジェクトを返す場合、配列の要素または集約クラスのメンバが中括弧のリストで初期化される場合。標準ライブラリのコンテナを初期化すると、insert メンバと push メンバが呼び出され、コンテナはその要素のコピーを初期化しますが、emplace メンバで作成された要素は直接初期化されます。
  • 引数と戻り値:コピーコンストラクタは非参照クラス型の引数を初期化するために使用されるため、コピーコンストラクタの引数は参照型でなければなりません。
  • コピー初期化の限界:使用する初期化には明示的なコンストラクタによる型変換が必要なので、コピー初期化を使用するか直接初期化を使用するかは関係ありません。
vector<int> v1(10);
vector<int> v2 = 10;//wrong
void f(vector<int>);
f(10);//wrong
f(vector<int>(10));
  • コンパイラはコピーコンストラクタをバイパスすることができます。
string null_book = "";//コピー初期化
string null_book("");//コピー・コンストラクタは省略されるが、コピー・ムーブ・コンストラクタは存在し、アクセス可能でなければならない。

コピー代入演算子

Sales_data trans,accum;
trans = accum;//売上の使用_dataのコピー代入演算子は
// 
class Foo{
public:
 Foo& operator=(const Foo&);
};//算術オブジェクトの左辺への参照を返す。

合成コピー代入演算子

Sales_data& Sales_data::operator=(const Sales_data &rhs){
 bookNo = rhs.bookNo;
 units_sold = rhs.units_sold;
 revenue = rhs.revenue;
 return *this;
}

デストラクタ

class Foo{
public:
 ~Foo();
};

3/5の法則

  • デストラクタを必要とするクラスは、コピー操作と代入操作も必要とします。
  • コピー操作が必要なクラスは代入操作も必要で、その逆も同様です。

use=default

  • コピー制御メンバを=defaultと定義することで、合成されたバージョンを生成するようコンパイラに明示的に要求することができます。
class Sales_data{
public:
 Sales_data() = default;
 Sales_data(const Sales_data&) = default;
 Sales_data& operator=(const Sales_data&);
 ~Sales_data() = default;
};
Sales_data& Sales_data::operator=(const Sales_data&) = default;
  • クラス内部で default を使用すると、合成された関数は暗黙的にインライン宣言されます。

ブロック・コピー

  • ほとんどのクラスでは、デフォルト・コンストラクタ、コピー・コンストラクタ、コピー代入演算子を暗黙的または明示的に定義する必要があります。
  • 削除を定義する関数は、コピーや割り当てを防止します。
  • 宣言はされていますが、使用することはできません。
  • クラスがデフォルトで構築、コピー、複製、破棄できないデータ・メンバを持つ場合、対応するメンバ関数は削除されたものとして定義されます。
  • 古いバージョンでは、コピーを防ぐためにprivate宣言が使われていました。
struct NoCopy{
 NoCopy() = default;
 NoCopy(const NoCopy&) = delete; //ブロックコピー
 NoCopy& operator=(const NoCopy&) = delete;//ブロック代入
 ~NoCopy() = default;
}
  • デストラクタを削除済みメンバにすることはできません。
struct NoDtor{
 NoDtor() = default;
 ~NoDtor() = default;
};
NoDtor nd; //エラーになると、デストラクタは削除される。
NoDtor *p = new NoDtor();//正しいが、pを削除することはできない。
delete p;//

コピー制御とリソース管理

  • クラスは値のように振る舞うことも、ポインターのように振る舞うこともできます。値のように振る舞う:オブジェクトは独自の状態を持ち、コピーは元のオブジェクトから完全に独立しています。ポインタのように振る舞う:状態を共有し、このクラスのオブジェクトをコピーするとき、コピーと元のオブジェクトは同じ基礎データを使用します。
  • 値のように振る舞うクラス
class HasPtr{
public:
 HasPtr(const string& s = string()):ps(new string(s)),i(0){}
 HasPtr(const HasPtr& p):ps(new string(*p.ps)),i(p.i){}
 HasPtr& operator=(const HasPtr&) 
 ~HasPtr() {delete ps;}
private:
 string *ps;
 int i;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs){
 auto newp = new string(*rhs.ps);
 delete ps;
 ps = newp;
 i = rhs.i;
 return *this;
}
  • ポインタのように振る舞うクラスの定義
//参照カウントを使用するクラスを定義する
class HasPtr{
public:
 HasPtr(const string& s = string()):ps(new string(s)),i(0),use(new size_t(1)){}
 HasPtr(const HasPtr& p):ps(p.ps),i(p.i),use(p.use){++*use;}
 HasPtr& operator=(const HasPtr&);
 ~HasPtr();
}
HasPtr::~HasPtr(){
 if(--*use == 0){
 delete ps;
 delete use;
 }
}
HasPtr& HasPtr::operator=(const HasPtr& rhs){
 ++*rhs.use;
 if(--*use == 0){
 delete ps;
 delete use;
 }
 ps = rhs.ps;
 i = rhs.i;
 use = rhs.use;
 return *this;
}

スワップ操作

class HasPtr{
 friend void swap(HasPtr&,HasPtr&);
};
inline void swap(HasPtr &lhs,HasPtr *rhs){
 using std::swap;
 swap(lhs.ps,rhs.ps);
 swap(lhs.i,rhs.i);
}
//swap関数はstdの代わりにswapを呼び出すべきである。::swap
void swap(Foo &lhs,Foo &rhs){
 std::swap(lhs.h,rhs.h); //標準ライブラリのswapを使う
}
void swap(Foo &lhs,Foo &rhs){
 using std::swap;
 swap(lhs.h,rhs.h); //swapのHasPtrバージョンを使う
}
//代入演算子でswapを使う
HasPtr& HasPtr::operator=(HasPtr rhs){
 swap(*this,rhs);
 return *this; //rhsは破棄されるため、rhsのポインタは削除される。
}

コピー制御の例

class Message{
 friend class Folder;
public:
 explicit Message(const string& str = ""):contents(str){}
 Message(const Message&);
 Message& operator=(const Message&);
 ~Message();
 
 //Folderコレクションへのメッセージの追加と削除
 void save(Folder&);
 void remove(Folder&);
private:
 string contents;
 set<Floder*> folders;
 void add_to_Folders(const Message&);
 void remove_from_Folders();
};
void Message::save(Folder& f){
 folders.insert(&f);
 f.addMsg(this);
}
void Message::remove(Folder& f){
 folders.erase(&f);
 f.remMsg(this);
}
void Message::add_to_Folders(const Message& m){
 for(auto f : m.folders)
 f->addMsg(this);
}
Message:Message(const Message& m):contents(m.contents),folders(m.folders){
 add_to_Folders(m);
}
void Message::remove_from_Folders(){
 for(auto f:folders)
 f->remMsg(this);
}
Message::~Message(){
 remove_from_Folders();
}
Message& Message::operator=(const Message& rhs){
 remove_from_Folders();
 contents = rhs.contents;
 folders = rhs.folders;
 add_to_Folders(rhs);
 return *this;
}
void swap(Message& lhs,Message& rhs){
 using std::swap;
 for(auto f:lhs.folders)
 f->remMsg(&lhs);
 for(auto f:rhs.folders)
 f->remMsg(&rhs);
 swap(lhs.folders,rhs.folders); //swap(set&,set&)
 swap(lhs.contents,rhs.contents); //swap(string&,string&)
 for(auto f:lhs.folders)
 f->addMsg(&lhs);
 for(auto f:rhs.folders)
 f->addMsg(&rhs);
}

動的メモリ管理クラス

class StrVec{
public:
 StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){}
 StrVec(const StrVec&);
 StrVec& operator=(const StrVec&);
 ~StrVec();
 
 void push_back(const string&);
 size_t size() const {return first_free - elements;}
 size_t capacity() const {return cap - elements;}
 string *begin() const{return elements;}
 string *end() const {return first_free;}
private:
 static std::allocator<string> alloc;
 void chk_n_alloc(){if(size()==capacity()) reallocate();}
 std::pair<string*,string*> alloc_n_copy(const string*,const string*);
 void free();
 void reallocate();
 string *elements;
 string *first_free;
 string *cap;
};
void StrVec::push_back(const string& s){
 chk_n_alloc();
 alloc.construct(first_free++,s);
}
pair<string*,string*> StrVec::alloc_n_copy(const string *b,const string *e){
 auto data = alloc.allocate(e - b);
 return {data,uninitialized_copy(b,e,data)};
}
void StrVec::free(){
 if(elements){
 for(auto p = first_free;p!=elements;)
 alloc.destroy(--p);
 alloc.deallocate(elements,cap-elements);
 }
}
StrVec::StrVec(const StrVec& s){
 auto newdata = alloc_n_copy(s.begin(),s.end());
 elements = newdata.first;
 first_free = cap = newdata.second;
}
StrVec::~StrVec(){free();}
StrVec& StrVec::operator=(const StrVec &rhs){
 auto data = alloc_n_copy(rhs.begin(),rhs.end());
 free();
 elements = data.first;
 first_free = cap = data.second;
 return *this;
}
void StrVec::reallocate(){
 auto newcapacity = size() ? 2 * size() : 1;
 auto newdata = alloc.allocate(newcapacity);
 auto dest = newdata;
 auto elem = elements;
 for(size_t i = 0;i != size();++i)
 alloc.construct(dest++,std::move(*elem++));
 free();
 elements = newdata;
 first_free = dest;
 cap = elements + newcapacity;
}

オブジェクトの移動

  • コピー操作の多くは元のオブジェクトの破壊につながるため、移動操作を導入することでパフォーマンスを劇的に向上させることができます。
  • 新しい規格では、移動可能である限り、コピー不可能な型を保持するためにコンテナを使用することができます。
  • 標準ライブラリ・コンテナ、文字列、shared_ptrクラスは、移動とコピーの両方に対応しています。ioクラスとunique_ptrクラスは移動できますが、コピーはできません。

右値参照

  • 新規格では、移動操作をサポートするために右値参照が導入されました。右値参照は&&で取得します。
  • これは、他のユーザーを持たない、破棄される予定のオブジェクトにのみバインドすることができます。通常の参照は左値参照と呼ばれます。左値は永続的で、右値は一時的です。
  • 右値は、リテラル値定数か、式の評価中に作成される一時オブジェクトです。右値参照を使用するコードは、参照オブジェクトのリソースを自由に引き継ぐことができます。
int i =42;
int &r = i;
int &&rr = i;//F
int &r2 = i * 42//F i*42 
const int &r3 = i*42;//T const参照は正しい値にバインドできる。
int &&rr2 = i*42;//T
  • 右値参照は、右値参照型の変数にバインドすることはできません。変数は永続的で、スコープを離れるまで破棄されません。
int &&rr1 = 42;
int &&rr2 = rr1;

標準ライブラリmove関数

  • 左値は、対応する右値の参照型に明示的に変換することができます。moveはコンパイラに左値があることを伝えますが、私はそれを右値のように扱いたいのです。
  • moveを呼び出すということは、rr1に値を代入するか、rr1を破棄する以外には、moveは二度と使われないということです。
int &&rr3 = std::move(rr1);

コンストラクタの移動と代入関数の移動

StrVec::Strvec(StrVec &&s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap){
	s.elements = s.first_free = s.cap = nullptr;
}
  • 新しいメモリを割り当てず、与えられたメモリを引き継ぎます。
Read next

Linuxでよく使われるls, cd, pwd, mkdirコマンドの説明

lsコマンドはLinuxで最もよく使われるコマンドの1つで、非常に古いコマンドでもあります。Linuxのコマンドを勉強し始めるときに、一つアドバイスをしておきます。ルート "内のLinuxで/特別なシンボルを使用しないようにしようとすることはできませんので、そのような:@#¥&#x26;()-、スペースなど。なぜならLinuxで...

Sep 22, 2020 · 5 min read