2.2K Views
May 23, 13
スライド概要
C++入門者向けに、STLのコンテナを中心に解説してあります。
株式会社ヘキサドライブの資料共有用アカウントです。 公式ブログ:https://hexadrive.jp/hexablog/ note :https://note.com/hexadrive
グリとブランの C++ 講座 ~ C++98 (STL) と、ほんのちょっとの C++11 ~ 株式会社ヘキサドライブ 原 龍 (ドラゴン)
最近はUnity等を使っての開発が浸透してきて、 C++ を書く機会が少なくなってる気がするニョ。 名前:グリ 名前:ブラン
確かにそうだけど、 実行速度を求められるゲームプログラムは まだまだ C++ で書くことも多いニャ。
時代がソーシャル要素を盛り込んだネットワークゲーム に移行しても、クライアントサイドはもちろんのこと、 サーバーサイドも C/C++ が最も使われてるんだニャ。
へー、それはなんでかニョ?
もちろん C++ じゃなくて GC が使える Java や C#、動的言語の Ruby や Python で書いたほうが 開発効率は格段に上がるニャ。
でも C++ と Java/C# だったらスループット性能で 10倍(※) 、C++ と Ruby/Python だったら 1000倍も違うと言われているんだニャ。 ※JITコンパイラを備えるJava VM では、Cよりも速くなることがあ るようです。 ただし、ネットワークに対する 入出力が多いゲームでは、シス テムコールを呼び出す前後での バッファオーバーフローの チェック等が毎回生じるため、C との速度差が生まれるとのこと。
おー、結構違うんだニョ。
それだけ違ったら、用意する サーバーの台数も変わってくるニャ。 サーバーの運用コストって高いから…。
確かに、できるだけお金はかけたくないニョ。
そうだニャ。だから C++ について見てみるニャ。
• 簡単に C++ の歴史と機能紹介 • Standard Template Library (STL) • 【C++】サンプル【書いてみた】
それじゃあ、さくっと C++ の歴史と機能を紹介するニャ。
簡単に C++ の歴史と機能紹介 As for C++ C++は、汎用プログラミング言語の一つである。 C言語の拡張として開発された。 拡張はクラスの追加に始まり、仮想関数、多重定義、多重継承、テンプレート、例外処理 といった機能が続いていった。 静的な型システムを持ち、手続き型プログラミング・データ抽象・オブジェクト指向プログラミン グ・ジェネリックプログラミングといった複数のプログラミングパラダイムをサポートするマルチパラダ イムプログラミング言語である。 詳しくは wiki を見てね! http://ja.wikipedia.org/wiki/C%2B%2B
簡単に C++ の歴史と機能紹介 • 初めての標準化 C++98 • STL に代表される標準ライブラリの追加 C++03 • C++ 98での不具合の修正等 • C++98以来初の大きな改訂 C++11 • マルチスレッドやジェネリックプログラミングを強化 詳しくは wiki を見てね! http://ja.wikipedia.org/wiki/C%2B%2B
簡単に C++ の歴史と機能紹介 クラス - Class オブジェクトの設計図。クラスから生成したオブジェクトをインスタンスという。 クラスにはインスタンス生成時に呼ばれるコンストラクタと、 インスタンスが破棄される時に呼ばれるデストラクタを定義できる。 また、クラス内の関数(メンバ関数)や変数(メンバ変数) のアクセスレベルも指定できる。 class Hoge { public: // どこからでもアクセス可能 Hoge(void) : _member(0) {} // コンストラクタ virtual ~Hoge(void) {} // デストラクタ protected: // 継承した子クラスからのみアクセス可能 void Function(void); private: //クラス内からのみアクセス可能 int _member; }; Hoge* hoge = new Hoge();
簡単に C++ の歴史と機能紹介 クラス - Class クラスを継承することで、親クラスの機能を持ち合わせたまま、子クラスで拡張することもできる。 class Parent { public: Parent(void) : _member(0) {} virtual ~Parent(void) {} protected: int _member; }; class Child: public Parent { public: Child(void) {} virtual ~Child(void) {} void OutputMember(void) { std::cout << _member << std::endl; } };
簡単に C++ の歴史と機能紹介
オーバーライド - Override
子クラスが親クラスのメンバ関数を上書きできる。
class Parent {
public:
void Function(void) { std::cout << "parent" << std::endl; }
};
class Child : public Parent {
public:
void Function(void) { std::cout << "child" << std::endl; }
};
Parent* parent = new Child();
Child* child = new Child();
parent->Function(); // parent
child->Function(); // child
簡単に C++ の歴史と機能紹介
仮想関数 - Virtual Function
メンバ関数をオーバーライドした子クラスを、親クラスの型へアップキャストしながら格納した時に、
メンバ関数の動作を上書きすることができる。
class Parent {
public:
virtual void Function(void) { std::cout << "parent" << std::endl; }
};
class Child : public Parent {
public:
void Function(void) { std::cout << "child" << std::endl; }
};
Parent* parent = new Child();
Child* child = new Child();
parent->Function(); // child
child->Function(); // child
簡単に C++ の歴史と機能紹介 Tips: デストラクタの落とし穴 継承される可能性のあるクラスのデストラクタは仮想関数にすること。 仮想関数にしないと、ポリモーフィズムを活用した際に想定外の挙動となることがある。 以下サンプル。 class Parent { public: Parent() { std::cout << "Parent Constructor" << std::endl; } ~Parent() { std::cout << "Parent Destructor" << std::endl; } }; class Child : public Parent { public: Child() { std::cout << "Child Constructor" << std::endl; } ~Child() { std::cout << "Child Destructor" << std::endl; } };
簡単に C++ の歴史と機能紹介 Tips: デストラクタの落とし穴 Child child; /* ▼出力(特に問題なし) Parent Constructor Child Constructor Child Destructor Parent Destructor */ Parent* pParent = new Child(); // 親の型にアップキャスト delete(pParent); /* ▼出力(宣言された型が優先されるため、子のデストラクタが呼ばれない!) Parent Constructor Child Constructor Parent Destructor */
簡単に C++ の歴史と機能紹介
純粋仮想関数 - Pure Virtual Function
実装が無い、宣言のみの仮想関数。継承して実装をしなければインスタンス化できない。
class Parent {
public:
virtual void Function(void) = 0;
};
class Child : public Parent {
public:
void Function(void) { std::cout << "child" << std::endl; }
};
//Parent* pParent= new Parent(); コンパイルエラー!
Parent* pParent= new Child();
pParent->Function(); // child
簡単に C++ の歴史と機能紹介
多重定義 - Overload
引数を変えた、同名の関数や演算子を複数定義できる。
class Hoge {
public:
void Function(int n) { std::cout << n << std::endl; }
void Function(float f) { std::cout << f << std::endl; }
};
Hoge hoge;
hoge.Function(1);
hoge.Function(5.8f);
簡単に C++ の歴史と機能紹介
テンプレート - Template
コンパイル時にコードを生成できる。
ジェネリックプログラミング(データ形式に依存しないプログラミング形式) に用いられる。
コンパイル時に生成されるため、使用を誤るとコード量自体は多くないのに
生成された実行ファイルのサイズが肥大化してしまうという危険性もあるため注意が必要。
template <typename T>
class Hoge {
public:
void Function(T value) {
std::cout << value << std::endl;
}
};
Hoge<int> hoge1;
hoge1.Function(1);
Hoge<float> hoge2;
hoge2.Function(0.3f);
簡単に C++ の歴史と機能紹介
テンプレートメタプログラミング
- Template Metaprogramming
テンプレートを応用して再帰的にコードを生成することにより、動的な計算量を削減することができる。
template <int N>
struct Factorial{
enum { value = N * Factorial<N - 1>:: value};
};
template <>
struct Factorial<0> {
enum { value = 1 };
};
// これらは実行時に計算することがないため、処理コストが少ない。
int x = Factorial<4>::value; // 24
int y = Factorial<0>::value; // 1
代表的なものはこれくらいかニャ。
あとは多重継承もあるけど、使ったら往々にして コードが複雑化してしまうから、割愛するニャ。 できるだけ単一継承と包含で対応するニャ。 ※多重継承を使うことでコードが 複雑化するというのは、あくまでも 私見です。使い所を誤らなければ 問題ありません。
C++ 自体が機能的に多いという事はなくて、 これらの機能を応用することで様々な案件に対応する 言語なんだニャ。テンプレートとかは奥が深いニャ。
聞いてるのかニャ? 聞いてますニョ~
じゃあ次は C++98 で標準化された、 Standard Template Library (STL) について解説するニャ。
Standard Template Library (STL) As for STL テンプレートを最大限に生かす構成を取っており、コンテナ・イテレータ(反復子)・アルゴリズ ム・関数オブジェクトと呼ばれる各要素から成っている。 C++におけるジェネリックプログラミングのはしりとなった。 オブジェクトを格納するコンテナとそれを操作するアルゴリズム、その接点としてイテレータが存 在し、コンテナとアルゴリズムが互いに完全に独立しているのも特徴の一つである。 詳しくは wiki を見てね! http://ja.wikipedia.org/wiki/Standard_Template_Library
Standard Template Library (STL) コンテナ:vector 連続要素を保持する動的配列。 STLの中では一番シンプルで有用な場面も多い。 リファレンスは http://www.cppll.jp/cppreference/cppvector.html を参照。 特徴的なのは、格納された要素がメモリ空間において隣接しているということ。 つまり、 &v[n] == &v[0] + n ということになる。 そのため、末尾への要素追加における計算量は定数時間 O(1) となる。 ちなみに中間への要素追加における計算量は線形時間 O(n) である。 vector はメモリの再割り当てを、それが必要になったタイミングで行う。 (末尾への要素追加時に領域が確保できていなかったら、一定の領域を確保するなど) 再割り当てを行う場合は一定のコストがかかるが、reserve を使用することにより、 再割り当ての機会を減らすことも可能。 API 呼び出しによるオーバーヘッドはあるが、malloc や new[] よりもメモリリークの回避や 配列アクセスにイテレータを使用できたりと利点が多いため、vector は多くの場面で活用できる。
Standard Template Library (STL) コンテナ:deque (Double Ended Queue) 双方向キュー。 簡単に言うと、vector を末尾への追加だけではなく、双方向に追加できるようにしたもの。 リファレンスは http://www.cppll.jp/cppreference/cppdeque.html を参照。 vector と同じく、先頭や末尾への要素追加は O(1) 、中間への要素追加は O(n)。 内部的にはリングバッファというデータ構造になっており、確保した領域の先頭アドレスに要素がある状態 で push_front した場合は、確保した領域の末尾に追加されるというイメージ。 先頭要素の位置は保持しているため、push_front したからといって全ての要素がメモリ空間上で 再配置されることはない。 先頭への要素追加/削除が行える点で vector よりも使い勝手が良いが 、 reserve ができない、要素がメモリ空間上で連続しているという保証はないという欠点もあるため、 どちらを採用するかは処理によって検討する必要がある。
Standard Template Library (STL) コンテナ:list 双方向連結リスト。 リファレンスは http://www.cppll.jp/cppreference/cpplist.html を参照。 vector や deque と違い、任意の位置への要素追加にかかる計算量が定数時間 O(1) となる。 しかし、連結リストでデータを保持するため、ランダムアクセスは遅い。(先頭、末尾からの探索となる) また、必要な分しかメモリを確保しないという特徴もある。 メモリキャッシュに乗らないので、deque を使ったほうがパフォーマンス的に上がる事が多いかもしれない。
Standard Template Library (STL) コンテナ:set 要素の重複を許さない集合。 リファレンスは http://www.cppll.jp/cppreference/cppset.html を参照。 要素の追加、検索、削除の計算量は対数時間 O(log n) となる。 vector と比べて重複を許さないというアルゴリズム的な差異があって使い勝手が良いが、 一般的に Red-Black ツリー(http://ja.wikipedia.org/wiki/%E8%B5%A4%E9%BB%92%E6%9C%A8) によって実装されており、それによりツリーを構築するために相当なオーバーヘッドがかかってしまう。 そのため、メモリキャッシュに乗ることも念頭に入れて、vector で重複チェックを行った上で 要素を追加する方がパフォーマンス的には優れているとも言える。
Standard Template Library (STL) コンテナ:map 連想コンテナ。キーと値を関連付けて管理する。 リファレンスは http://www.cppll.jp/cppreference/cppmap.html を参照。 set と同じで Red-Black ツリーなので、要素の追加、検索、削除の計算量は 対数時間 O(log n) となる。 使用する際のデメリットも set と同様。 しかし、キーを指定の型に設定できることはメリットとして大きいため、使用したい場面も多い。
Standard Template Library (STL) コンテナアダプタ:stack 末尾からの挿入と末尾からの取り出しをサポート。 Last In First Out (LIFO) とも呼ばれる。 リファレンスは http://www.cppll.jp/cppreference/cppstack.html を参照。 コンテナアダプタ:queue 末尾からの挿入と先頭からの取り出しをサポート。 First In First Out (FIFO) とも呼ばれる。 リファレンスは http://www.cppll.jp/cppreference/cppqueue.html を参照。 ※コンテナアダプタとはコンテナを内部に保持して、限定的に機能を公開しているもの。 単体では使いにくいが、protected 継承することにより、内部のコンテナにアクセスできる。 それにより独自の拡張が可能となっている。
Standard Template Library (STL) イテレータ - Iterator イテレータはポインタと同じような使い方でコンテナの各要素にアクセスするために用意される。 イテレータの中にもいくつか種類があるが、代表的なものは以下のものである。 ▼前方イテレータ - forward_iterator 要素の読み書きが行える。ただし前方への移動しかできない。 ▼双方向イテレータ - bidirectional_iterator 双方向へ移動しながら要素の読み書きが行える。 list や set、map はこれ。 ▼ランダムアクセスイテレータ - random_iterator ランダムアクセスが可能な双方向イテレータ。 vector や deque はこれ。
Standard Template Library (STL) アルゴリズム - Algorithm コンテナに対する汎用的な処理を集めたもの。 非常に多くの関数が用意されているため、STLを使用する際の恩恵として大きい。 カテゴリとして、四種類に分類される。 ・検索・操作 (http://www.cppll.jp/cppreference/cpp_algo_find.html) ・配列操作 (http://www.cppll.jp/cppreference/cpp_algo_manip.html) ・ソート関連 (http://www.cppll.jp/cppreference/cpp_algo_sort.html) ・その他 (http://www.cppll.jp/cppreference/cpp_algo_other.html)
Standard Template Library (STL) 関数オブジェクト - Function Object 関数呼び出し演算子をオーバーロードすることにより、オブジェクトでありながら関数のように呼び出せる。 関数ポインタやポリモーフィズムの代わりになり得る。 アルゴリズムの中には関数オブジェクトを引数で与えるものもあり、 それによって更にアルゴリズムの汎用性を向上させている。 また、C++11 から標準化されたラムダ式を使うことで、より簡潔に書ける。
STL はとても便利だけど、使い所を誤ると 実行速度の低下やリソースの無駄遣いに繋がるから 気を付けるニャ。
また、コンテナに関する箇所は私見で書いている ところもあるから、色々な人の意見を参考にしたり、 自分でも考えて使うようにしてほしいニャ。
便利なものこそ、よく考えて使えってことだニョ。
その通りだニャ。 最後にいくつかサンプルを載せるニャ。
【C++】サンプル【書いてみた】 公開するインターフェースクラスと実装部分を切り離す // インターフェースクラス。公開する。 class IModel { public: IModel(void) {} virtual ~IModel(void) {} virtual void SetPosition(const Vector3& position) = 0; virtual const Vector3& GetPosition(void) const= 0; };
【C++】サンプル【書いてみた】 公開するインターフェースクラスと実装部分を切り離す // 実装部分。公開しない。 class Model : public IModel { public: Model(void) : _position() {} virtual ~Model(void) {} void Update(); void Draw(); void SetPosition(const Vector3& position) { this->_position = position; } const Vector3& GetPosition(void) const { return this->_position; } private: Vector3 _position ; };
【C++】サンプル【書いてみた】 公開するインターフェースクラスと実装部分を切り離す // モデル作成クラス class ModelCreator { public: static IModel* Create(void); }; // new Model(); // ポリモーフィズム IModel * pModel = ModelCreator::Create();
【C++】サンプル【書いてみた】
リストにランダムな整数値を入力し、閾値を基準に削除する
typedef std::vector<int> IntList;
IntList list;
list.push_back(1000);
for (int i = 0; i < 10 ; ++i) {
list.push_back(std::rand());
}
std::sort(list.begin(), list.end());
IntList::iterator it = std::find(list.begin(), list.end(), 1000);
list.erase(it, list.end());
【C++】サンプル【書いてみた】
ポリシークラス - Policy Class
構文的に同じインタフェースの下で、異なる処理を提供する。
ポリシーを使うクラスは、それが使うそれぞれのポリシーに対してひとつのテンプレート引数を持って、
テンプレート化されている。 このため必要なポリシーを選択することが出来る。
struct NoChecking {
template <typename T>
static bool CheckPointer(T*) { return true; }
};
struct NullChecking {
template <typename T>
static bool CheckPointer(T* p) { return (p != nullptr); }
};
struct BadPointerDoNothing {
template <typename T>
static T* HandleBadPointer(T* p) { std::cout << "pointer is moldy" << std::endl; return
p; }
};
【C++】サンプル【書いてみた】
ポリシークラス - Policy Class
template <typename T,
typename CheckingPolicy = NoChecking,
typename BadPointerPolicy = BadPointerDoNothing>
class PointerWrapper {
public:
PointerWrapper() : _value(nullptr) {}
explicit PointerWrapper(T* p) : _value(p) {} // 暗黙的呼び出しを禁止
operator T*() {
if (!CheckingPolicy::CheckPointer(_value)) {
return BadPointerPolicy::HandleBadPointer(_value);
} else {
return _value;
}
}
private:
T* _value ;
};
【C++】サンプル【書いてみた】
ポリシークラス - Policy Class
PointerWrapper<int> num1(new int);
*num1 = 42;
std::cout << "num1:" << *num1 << std::endl;
// これは実行時にNULLアクセスによるエラーになる。
//PointerWrapper<int> num2;
//*num2 = 42;
//std::cout << "num2:" << *num2 << std::endl;
// これもエラーになるが、NULLアクセス前に pointer is moldy と出力する。
//PointerWrapper<int, NullChecking> num3;
//*num3 = 42;
//std::cout << "num3:" << *num3 << std::endl;
おまけとして、機能紹介って意味で C++11 のサンプルコードを載せるニャ。 詳細な解説は、また別の機会にするニャ。
【C++】サンプル【書いてみた】 型推論 // イテレータの型は複雑 std::vector<int>::iterator it = vector.begin(); ↓ // このような形で型判別をコンパイラ任せることで、可読性を上げ、冗長性を省く auto it = vector.begin(); // また、decltypeもひとつの選択肢として用意されている。 int foo; decltype(foo) bar = 1; // fooと同じ型でコンパイル時に決定される
【C++】サンプル【書いてみた】 NULLポインタ 定数 0 には整数定数と NULL ポインタという二つの側面がある。 これまで NULL はプリプロセッサマクロで 0 となるように定義されてきた。 しかし 0 はあらゆるポインタ型への変換が許されているため、オーバーロードの使い方によっては 意図した動作をしないことがある。 void foo(char *); void foo(int); foo(NULL); // foo(int) が呼ばれる! そのため、新たな予約語として nullptr が用意された。 nullptr は整数型との比較や代入は禁止されているが、NULL と同様にあらゆるポインタ型への 変換が許されている。 今後、NULL が非推奨となる可能性もあるので、nullptr を使うように心掛けたい。
【C++】サンプル【書いてみた】
ラムダ式
// 関数オブジェクトと同じように使える
void* ptr = NULL;
auto isNullptr = [](void* ptr) { return ptr == nullptr; };
if (isNullptr(ptr)) {
std::cout << "ptr is null." << std::endl;
}
// その場で呼び出すこともできる
int square2 = [](int x) { return x * x; } (2);
std::cout << square2 << std::endl; // 4
// Hello,Worldをこんな書き方することも可能。特にこうする意味は無いけど。
[]() { std::cout << "Hello,World" << std::endl; } ();
【C++】サンプル【書いてみた】
ラムダ式を使ってクロージャ
int sum = 0;
std::vector<int> list;
list.push_back(1);
list.push_back(2);
list.push_back(3);
std::for_each(list.begin(), list.end(), [&sum](int x) {
sum += x; // このラムダ式が宣言されているスコープの変数にアクセス可能
});
std::cout << sum<< std::endl;
// 6
お疲れ様でした。 それではまた次の機会に。