C++講座++

この記事について

この記事はCCS Advent Calendar 2019 - Adventarの6日目の記事です。

前日記事:Steam,Switch,PS4で最高なインディーズゲーム7選+α - 甘口ピーナッツ

はじめに

弊サークルのC++講座を履修してはや2年、講座ではやらなかったことに直面し、ほえ〜〜〜〜〜〜ってなってきました。
それを解説していこうと思います。
といってもどれも深くまではやってないので目次みて知ってる〜〜〜ってなったやつはみる必要ないと思います。
なんなら全部知らなくてもゲームは余裕に作れちゃうからそれよりもゲー制頑張ってくださいってお気持ち。( ˘ω˘)
しかも非推奨なものまである・・・。
あとC++講座受講勢は最初の4項以外は講座全部終わった後にみたほうがいいかも。

知らね〜〜〜〜〜〜

右辺値参照

下のようなかんじで使う左辺値参照はC++講座で出てきたと思います。

auto& x = zoo.bird.pengin;

左辺値(その名の通り左辺に置くことができるやつ)(クソ雑に言うと変数(const修飾された定数も含む))に対して右辺値への参照というものがあります。
右辺値とは関数の返り値のようなすぐに消えてしまうオブジェクトのことをいいます。
左辺値参照では型の後に&をつけていましたが、右辺値参照では&&をつけます。
つまり以下の通り。(add関数は2つの引数の和を返す関数)

int&& x = add(2, 10);

正直この程度のことで右辺値参照を使う必要はありませんが、返り値が巨大なクラスだったりすると戻り値から代入先変数へわざわざコピーする必要がないため非常に有効です。
また、add関数の返り値(のオブジェクト)は本来はすぐに消えるはずでしたが、これにより x が消えるまでは永らえることができました。
そのため、右辺値参照は一時オブジェクトの延命をさせるとも言われます。

std::array

C++講座ではvector以外のSTLは利用されなかったけどSTLはももはらさんが解説してくれると思うので、他のSTLと比べて特殊感が薄いし競プロでは活躍しなそうなこいつだけ。
std::arrayは固定長の配列でstd::array<[型], [要素数]>ってかんじで使う。
つまり[型] array[要素数]。
じゃあ別に普通の配列でよくね?ってかんじがするかもしれないけどこいつはちゃんと要素数が決まっていて、しかも配列として使っていることを保証できる。
そのため関数の引数にとる固定長配列の要素数コンパイル段階で制限することができるし、2重配列も容易に渡すことができる、最強。
しかも普通の配列はポインタと区別がつかないのに対してそこも明らかになってる。

基本的にC++では固定長配列はstd::array、可変長配列はstd::vectorを利用するのが吉。
ただし、ライブラリによってはそれよりも推奨されているやつがあるときがあるのでその場合はそちらを。(例えばSiv3dでは固定長配列ではstd::arrayを、可変長配列ではArrayを推奨している。)

拡張for

従来のfor文では配列を回すときにこうしてたはず。

for(int i = 0; i < ary.size(); i++) {
	// なんか処理
}

これを拡張forで書くとこうなる。

for(auto i : ary) {
	// なんか処理
}

最強に便利なのがmapやlistなどもこれで回せるということ、最強に便利。

でも実はこれ、要素の書き換えができない。
コピーして回してるのかと思ってたけどどうも違うらしい。(よくわかってない顔。)
auto&やauto&&にすることで書き換え可能になる。(よくわかってない顔。)

const修飾とポインタ修飾

これは私がC++講座を受けたときはポインタ完全理解幼女先輩によって解説されましたが、18以降はどうかしらないので一応。
ポインタ修飾がないとき、const は型名の前につけても後につけてもどっちでもいいです。
つまり以下のやつは両方ともおけおけおっけー

const int A = 10;
int const B = 53;

*がついてても同じよ〜って行きたいところですが、書き換えをさせないというのはポインタの場合2つの対象があります。

  1. ポインタの指す先を参照したオブジェクト
  2. ポインタの指す先

ポインタの指す先を参照したオブジェクトをconstにする場合はconst [型]*となります。
[型] const*でも大丈夫。
こんなかんじ。

const int a = 3;
const int b = 44;
const int* p1 = &a;
//*p1 = 0; エラー、参照先を書き換えてはならない
p1 = &b; // 指す先は変更可能なのでおけおけおっけー
//int* p2 = &a; エラー、参照先が書き換えられてしまう可能性があるのでconst intのポインタを渡すことはできない

対してポインタの指す先をconstにする場合は [型]* const となります。

int a = 3;
int b = 44;
int* const p1 = &a;
p1 = &b; // 指す先は書き換え可能なのでおけおけおっけー

const int c = 0;
// int* const p2 = &c; エラー、参照先は書き換え不可能なのでダメ、しかもc はconst int なので二重にダメ

指す先も参照先も書き換え内容にする場合は上記2つを合わせて const [型]* const 、または [型] const* const となります。
const の位置に注目すると結構むずいですがポインタ修飾記号に注目すると幸せに慣れます。
*を「〜へのポインタ」と考えればconst [型]* や [型] const*はconst [型]へのポインタ、[型] constへのポインタとなり、[型]* const は[型]へのポインタでconstとなります。

この考えで行けばポインタへのポインタで一部が const のときも容易👼に考えることができます。(なお見た目)
const修飾以外のvolatile修飾などが絡んできても同様。

メンバ関数へのポインタ

メンバ関数ポインタ

何かと便利な関数ポインタ、staticなメンバ関数だと普通に入れることができますが、非staticなものだとエラーを起こします。
これは非staticなメンバ関数はオブジェクトの持つメンバ変数を利用する可能性があるからです。
なのでメンバ関数へのポインタを扱いたい場合は、メンバ関数ポインタと呼ばれる特殊な関数ポインタを用いる必要があります。

#include <iostream>

using namespace std;

class Number {
private:
	int mA;

public:
	Number(const int a) : 
		mA(a) {}

	int getA() const {
		return mA;
	}

};

int main() {
	Number a(01);
	int (Number::*f)(void) const = &Number::getA;

	cout << (a.*f)() << endl;

	return 0;
}

こうなるんですが、つらつらのつら。
もっとどうにかならんのか。

functionクラス

メンバ関数ポインタを気軽に使えるようにしてくれるのがfunctionクラス。

#include <iostream>
#include <functional>

using namespace std;

class Number {
private:
	int mA;

public:
	Number(const int a) : 
		mA(a) {}

	int getA() const {
		return mA;
	}

};

int main() {
	Number a(01);
	function<int()> f = bind(&Number::getA, &a);

	cout << f() << endl;

	return 0;
}

普通の関数ポインタにみたいに使えるやったぜ。
ちなみに本物の普通の関数ポインタに対してもfunctionは使える。

ラムダ式でキャプチャ

ラムダ式のキャプチャ指定を&にすることで他の変数を参照してラムダ式内で利用することできる。
これを利用すればさらに楽になる。

#include <iostream>

using namespace std;

class Number {
private:
	int mA;

public:
	Number(const int a) : 
		mA(a) {}

	int getA() const {
		return mA;
	}

};

int main() {
	Number a(01);
	auto f = [&]() { return a.getA(); };

	cout << f() << endl;
	
	return 0;
}

神、ゴッドマキシマムゲーマーレベルビリオンまである。

テンプレート

vectorは中身の型を指定できますが、あれのことです。
クラスや関数をテンプレートにすることで型だけ違うような同一処理を行うクラス簡単に錬成できます。
テンプレートに関することだけで本が作れるレベルの沼分野なので超入門レベルだけ。(そもそも深淵部分はまだ私自身が勉強してない。)
おじょうさんのテンプレート講座を待ちましょう。(結構深いところまでやっていく予定らしいです。)

関数テンプレート

関数をテンプレート化することで引数の違うほぼ同じことをしている関数を作らなくてすむ。
たとえばこんなかんじのint型のadd関数があったとして

int add(const int a, const int b) {
	return a + b;
}

これをテンプレート化するとこうなる。

template<class T>
T add(const T a, const T b) {
	return a + b;
}

使うときはadd<[型]>([値], [値])。(引数で判別できる場合は<[型]>は省略可能。)
メンバ関数をテンプレート化したやつはメンバ関数テンプレートと呼ばれるけどまあ一緒。

クラステンプレート

vectorをはじめとするSTLはこれ。
こんなかんじのint型の2次元ベクトル構造体があったとして

struct Vec2 {
	int x, y;

	Vec2(const int x_, const int y_) :
		x(x_), y(y_) {}

	Vec2() = default;
};

これをテンプレート化するとこう。

template<class T>
struct Vec2 {
	T x, y;

	Vec2(const T x_, const T y_) :
		x(x_), y(y_) {}

	Vec2() = default;
};

使うときはVec2<[型]>。
例では構造体だったけどクラスでも全く同じ。

エイリアステンプレート

実はもう一つある。
エイリアステンプレートはテンプレートに対して別名をつけることができるやつで、テンプレート引数の一部を指定させた状態にすることもできる。

こんなかんじ

template<class T>
using vec = std::vector<T>;

template<class T, class U>
using rmap = std::multimap<T, U, std::greater<T>>;

これでVec<[型]>でvectorが使えるようになって、rmap<[型1], [型2]>で降順ソートのmapが使えるようになります。
同じく型の別名付けに用いられるtypedefでは同じようなことはできません。
ところでmultimap使うの久々すぎて覚えてなかったんですが、multimapって添字演算子使えないんですね・・・。

注意点

今回はtemplateと書きましたがtemplateでもいけます。(typenameには他にも使う場面があるのですが、当記事では割愛。)

テンプレートはファイル分けをするときに実装をヘッダーファイルに記述にしなくてはなりません。
というのも、テンプレートクラスのソースファイルのコンパイル時には実際にどの型として使われるのかわからないからです。
そのため実際に使うそのテンプレートを使用するファイルをコンパイルするときに指定型のそのクラスを作ってもらう必要があり、そこで実装部分が必要となります。
どの型は確実に使うのかを指定することにより指定済みの型バージョンについては実装を分離させたり、他の型のときとは違う処理をさせることもできますがそこらへんはおじょうさんの講座を待ちましょう。

演算子オーバーロード

2次元ベクトルを扱う構造体やクラスを考えるとき、必要そうなのは演算です。
add関数などを作って引数にとった別オブジェクト(とは限らないけど)の成分を加えるようにすればできますが、もっと足し算っぽく書きたい。
こういうのができたら最高最善最大王。

Vec2 a(8, 1), b(11, 2);
Vec2 c = a + b;
std::cout << c.x << " " << c.y << std::endl;

出力

19 3

そういうときに演算子オーバーロード演算子に対する振る舞いを定義できます。

二項演算子

A + B、A / B、A & B、のような形の演算子オーバーロードするときはこうなる。(3分クッキング並)

#include <iostream>

using namespace std;

struct Vec2 {
	int x, y;

	Vec2(int x_, int y_) :
		x(x_), y(y_) {}

	Vec2() = default;

	Vec2 operator +(const Vec2& obj) const {
		Vec2 ret;
		ret.x = this->x + obj.x;
		ret.y = this->y + obj.y;
		return ret;
	}
};

int main() {
	Vec2 a(8, 1), b(11, 2);
	Vec2 c = a + b;

	cout << c.x << " " << c.y << endl;
	return 0;
 }

だいたい普通の関数と一緒で、[戻り値の型] operator [演算子] ([引数(演算子の右側にくるオブジェクト)]) となる。
演算子前後には空白あってもなくてもおっけー。
引数や戻り値の方は被演算オブジェクトと同じじゃなくても大丈夫なので、今回の2次元ベクトルの例だとスカラー倍させる *演算子内積を返す *演算子などを定義することもできる。
ただし、オーバーロードなので引数の型は同じで返り値の型は違うようにはできない。(つまり今回の例だと必要になりそうな、ベクトルの内積外積のどちらか片方は諦めて普通の関数を使わなくてはならない。)

添字演算子

xとyに対して配列的なアクセスをしたい!というときは添字演算子オーバーロード
添字演算子というのは配列のときの []。
以下3分クッキング。

#include <iostream>
#include <stdexcept>

using namespace std;

struct Vec2 {
	int x, y;

	Vec2(const int x_, const int y_) :
		x(x_), y(y_) {}

	Vec2() = default;

	int& operator [](const int n) {
		if(n == 0) {
			return x;
		}
		else if(n == 1) {
			return y;
		}
		else {
			throw out_of_range("over index");
		}
	}
};

int main() {
	Vec2 a(7, 53);
	a[0] = 10;
	cout << a[0] << " " << a[1] << endl;
	return 0;
}

出力

10 53

添字は入力に注意。

単項演算子

ここまでは引数といえるものがあるやつでしたが、明らかにそういうのがない演算子があります。
++と--です。しかもこいつ、変数の前後で処理が微妙に違う。
じゃあどうするのかというと・・・ちょっと頭悪いかんじでした。

#include <iostream>

using namespace std;

struct Vec2 {
	int x, y;

	Vec2(const int x_, const int y_) :
		x(x_), y(y_) {}

	Vec2() = default;

	//	後置インクリメント
	Vec2 operator ++(int n) {
		Vec2 ret = *this;
		this->x++;
		this->y++;
		return ret;
	}

	//	前置インクリメント
	Vec2 operator ++() {
		++this->x;
		++this->y;
		return *this;
	}
};

int main() {
	Vec2 a(0, 0), b(0, 0);
	Vec2 c = a++;
	Vec2 d = ++b;

	cout << "a: " << a.x << " " << a.y << endl;
	cout << "b: " << b.x << " " << b.y << endl;
	cout << "c: " << c.x << " " << c.y << endl;
	cout << "d: " << d.x << " " << d.y << endl;
 	return 0;
}

出力

a: 1 1
b: 1 1
c: 0 0
d: 1 1

ゴリ押しだった・・・。
使うことはないですがこのnには0が入るらしいです。

注意点

演算子は関数名みたいなものなので中身の記述には何も制限がありません。
つまり +演算子だけど掛け算をさせるとかもできますがちゃんと演算子の本来の意味をなしてる処理をさせましょう。
あれ、std::coutさん・・・?

仮想継承

C++は複数のクラスを基底クラスとする多重継承が可能、やったぜ。
でもこれはとんでもない災厄を招くことが・・・。

f:id:yuma1338:20191205185921p:plain
上のような継承関係になってるときがまずい。(ダイヤモンド継承とかひし形継承とか呼ばれる。)
BによってAの一部の関数がオーバーライドされて、CによってAの他の関数がオーバーライドされて、BとCをDに継承させることで両方ともオーバーライドされた状態が完成!。
ってしたいのはわかるが、この場合AはBとCで共通ではなく、Bによって継承されたAと、Cによって継承されたAが混在した状態になる。

f:id:yuma1338:20191205185933p:plain
上のような図になっているということ。
Aが重複してて厄介だしメモリも無駄、これを解消するのが仮想継承。
BとCによるAの継承を通常継承ではなく、仮想継承にすることでAの実態生成を保留にできる。
その後、BとCをDに通常継承させることで唯一のAが爆誕

コードにするとこう。

#include <iostream>

using namespace std;

class A {
private:
	int mA;

public:
	A(const int a) :
		mA(a) {}

	virtual void hoge() const = 0;
	virtual void huga() const = 0;

	int getA() const {
		return mA;
	}
};

class B : virtual public A {
public:
	//	仮想継承する場合は基底クラスのコンストラクタは呼び出さない
	B() {}

	void hoge() const override {
		cout << "ほげええええええ" << endl;
	}
};

class C : virtual public A {
public:
	//	仮想継承する場合は基底クラスのコンストラクタは呼び出さない
	C() {}

	void huga() const override {
		cout << "ふがああああああ" << endl;
	}
};

class D : public B, public C {
public:
	//	仮想継承したやつを通常継承するときにAのコンストラクタを呼び出す
	D(): A(555) {}

	void hoge() const override {
		B::hoge();
	}

	void huga() const override {
		C::huga();
	}
};

int main() {
	D d;
	cout << d.getA() << endl;
	d.hoge();
	d.huga();

	return 0;
}

出力

555
ほげええええええ
ふがああああああ

virtual [アクセス修飾子] で仮想継承にできる。
出力結果じゃわかりにくいけどブレイクポイントとか使うとちゃんとAが一つしかないことがわかる。

これで厄介な問題は解決した・・・したけどこれはあくまで苦肉の策でダイヤモンド継承自体がそもそもカス。
殆どの場合は分割継承(?)される関数を別クラス化して解決できるはずなのでそのほうが圧倒的にいい。

おまけ

テンプレート引数にテンプレート

弊学の端末で下記のようなコードを書いたらコンパイラに怒られた。

vector<vector<int>> a;

テンプレート引数にテンプレートを入れる場合は外側の<>の内側に空白が必要らしい。
C++11より前ではそんな仕様だったらしい。
いや、デフォがC++03ってどういうこと・・・。
C++14以降に関しては入ってすらいない。
弊学は留学とか言う前にすべきことがあるんじゃないですかねと思いましたまる

終わりに

猫になりたい。