1 of 57

型の力を抜いて学ぶC++

@lapis_tw 2014-07-12 Sapporo.cpp

2 of 57

自己紹介

岩崎純一(らぴす:@lapis_tw)

本業:オーディオプログラマー

今回は音の話はしません。型の話をします。

3 of 57

本セッションの対象者

普段C++を使っていないプログラマ向けに話します。

より詳細な情報を知りたい方は以下の書籍(と規格書)を!

4 of 57

型の力を抜く

C++は型安全ではない強い型付け言語ですが、たまに型を無視して扱いたい場合があります。

そこで、型安全に型を消去することが出来る仕組みが、

TypeErasure(型消去)

です。

こんな手法を解説します。

5 of 57

TypeErasureの用途例

それぞれのクラスには継承関係が成り立っていないが、

一部の共通する機能があるため、纏めて扱いたい場合

class Module {

void Process();

};

class Plugin {

void Process();

};

std::vector<process_holder_t>

processers;

for ( auto& processor : processors ) {

}

6 of 57

型の力を抜く方法

・void*を用いる方法

・仮想関数テーブルを用いる方法

・基底クラスとテンプレートを組み合わせる方法

7 of 57

void*を用いる方法

8 of 57

void*を用いる方法

void*を用いて型情報を消去してみます。

struct Hoge {

void do_something() { ... }

};

Hoge* hoge = new Hoge();

void* void_pointer = hoge;

static_cast<Hoge>(void_pointer)->do_something();

9 of 57

void*を用いる方法

void*を用いて型情報を消去してみます。

struct Hoge {

void do_something() { ... }

};

Hoge* hoge = new Hoge();

void* void_pointer = hoge;

static_cast<Hoge>(void_pointer)->do_something();

ここでvoid*に押し込んで隠蔽

10 of 57

void*を用いる方法

void*を用いて型情報を消去してみます。

struct Hoge {

void do_something() { ... }

};

Hoge* hoge = new Hoge();

void* void_pointer = hoge;

static_cast<Hoge>(void_pointer)->do_something();

ここでvoid*に押し込んで隠蔽

キャストして元の型に戻してメンバ関数を呼ぶ

11 of 57

型情報を持たない手法の問題点

この手法は、

・明示的にキャストする必要がある

・元の型情報をユーザが知っていなければならない

ため、型安全を満たしていません。

12 of 57

型情報を持たない手法の問題点

struct hoge

{

hoge() { std::cout << "construct" << std::endl; }

~hoge() { std::cout << "destruct" << std::endl; }

};

hoge* hoge_pointer = new hoge();

void* void_pointer = hoge_pointer;

delete void_pointer;

正常に動作しない例

13 of 57

型情報を持たない手法の問題点

struct hoge

{

hoge() { std::cout << "construct" << std::endl; }

~hoge() { std::cout << "destruct" << std::endl; }

};

hoge* hoge_pointer = new hoge();

void* void_pointer = hoge_pointer;

delete void_pointer;

正常に動作しない例

ここで型情報が失われている

14 of 57

型情報を持たない手法の問題点

struct hoge

{

hoge() { std::cout << "construct" << std::endl; }

~hoge() { std::cout << "destruct" << std::endl; }

};

hoge* hoge_pointer = new hoge();

void* void_pointer = hoge_pointer;

delete void_pointer;

正常に動作しない例

ここで型情報が失われている

これは未定義の動作を引き起こす

15 of 57

仮想関数テーブルを用いる方法

16 of 57

仮想関数テーブル

継承を用いて多態性を表現する場合、その実装は処理系依存となっていますが、一般的に仮想関数テーブルを用いられています。

17 of 57

仮想関数テーブル

class Shape

{

points_t points;

public:

uint32_t count();

virtual void draw() =0;

virtual void move();

};

class Circle : public Shape

{

double r;

public:

void draw() override;

double get_radius();

};

18 of 57

仮想関数テーブル

class Shape

{

uint32_t count();

virtual void draw() =0;

virtual void move();

};

class Circle : public Shape

{

...

void draw() override;

double get_radius();

};

基底クラス

派生クラス

それぞれdraw()及びmove()が仮想関数になっています。

19 of 57

仮想関数テーブル

class Shape

{

uint32_t count();

virtual void draw() =0;

virtual void move();

};

class Circle : public Shape

{

...

void draw() override;

double get_radius();

};

基底クラス

派生クラス

オーバーライド

Circleではdrawのみをオーバーライドしています。

20 of 57

仮想関数テーブル

class Shape

{

uint32_t count();

virtual void draw() =0;

virtual void move();

};

class Circle : public Shape

{

...

void draw() override;

double get_radius();

};

基底クラス

派生クラス

オーバーライド

それぞれvirtual指定をしていないメンバ関数が1つずつあります。

21 of 57

仮想関数テーブル

Circle circle;

circle.draw();

circle.move();

Circle

draw()

Shape

draw()

move()

メンバ関数を呼び出した際の挙動

22 of 57

仮想関数テーブル

Circle circle;

circle.draw();

circle.move();

Circle

draw()

Shape

draw()

move()

メンバ関数を呼び出した際の挙動

23 of 57

仮想関数テーブル

Circle circle;

circle.draw();

circle.move();

Circle

draw()

Shape

draw()

move()

メンバ関数を呼び出した際の挙動

動的ディスパッチによって決定

24 of 57

仮想関数テーブル

Shape vtbl

Circle vtbl

Shape::move

Shape::draw

Shape::move

Circle::draw

slot

table

override

class Circle

class Shape

vptr

vptr

各クラスと仮想関数テーブルの関係

・各クラスは仮想関数テーブルをvptrによって参照しています

・仮想関数テーブルは各クラスごとに仮想関数があれば一つずつ登録されていきます

25 of 57

仮想関数テーブルを用いた実装

次に仮想関数テーブルを用いた実装を紹介します。

尚、コードはC++テンプレートテクニック第2版のサンプルコードとほぼ同様のものとなっております。

26 of 57

仮想関数テーブル::余談

C++の生みの親であるBjarneはPPPUCPPでこのように述べてます

"なぜvtblとメモリのレイアウトに言及するのはなぜか。オブジェクト指向プログラミングを利用するためにどれだけそれを知っている必要があるのだろうか。そうではないが、多くの人(特に私たち)は何がどのように実装されるのかに興味津々であり、人々が何かを理解していない時に都市伝説が生まれる。仮想関数は「コストがかかる」と言って手を出そうとしない人に出会ったことがある。なぜか。どれくらいコストがかかるのか。何と比べてそうなのか。

コストはどこで問題になるのか。仮想関数の実装モデルを説明したのは、そうした不安を抱かないようにするためだ。"

27 of 57

仮想関数テーブルを用いた実装

class drawable {

struct vtable {

void (*draw)(void*);

};

template <class T>

struct vtable_initializer {

static vtable vtable_:

static void draw(void* this_) {

static_cast<T*>(this_)->draw();

}

};

void* this_;

void* vptr_;

...

28 of 57

仮想関数テーブルを用いた実装

class drawable {

struct vtable {

void (*draw)(void*);

};

template <class T>

struct vtable_initializer {

static vtable vtable_:

static void draw(void* this_) {

static_cast<T*>(this_)->draw();

}

};

void* this_;

void* vptr_;

...

仮想関数テーブルクラス

29 of 57

仮想関数テーブルを用いた実装

class drawable {

struct vtable {

void (*draw)(void*);

};

template <class T>

struct vtable_initializer {

static vtable vtable_:

static void draw(void* this_) {

static_cast<T*>(this_)->draw();

}

};

void* this_;

void* vptr_;

...

型ごとの仮想関数テーブル生成用テンプレートクラス

30 of 57

仮想関数テーブルを用いた実装

public :

template <class T>

drawable(T& other) :

this_(&other),

vptr_(&vtable_initializer<T>::vtable_) {}

void draw() {

vptr_->draw();

}

};

template <class T>

drawable::vtable drawable::vrable_initializer<T>::vtable_ = {

drawable::vtable_initializer<T>::draw

};

31 of 57

仮想関数テーブルを用いた実装

public :

template <class T>

drawable(T& other) :

this_(&other),

vptr_(&vtable_initializer<T>::vtable_) {}

void draw() {

vptr_->draw();

}

};

template <class T>

drawable::vtable drawable::vrable_initializer<T>::vtable_ = {

drawable::vtable_initializer<T>::draw

};

コンセプトを満たすインスタンスを受ける

テンプレートコンストラクタ

32 of 57

仮想関数テーブルを用いた実装

public :

template <class T>

drawable(T& other) :

this_(&other),

vptr_(&vtable_initializer<T>::vtable_) {}

void draw() {

vptr_->draw();

}

};

template <class T>

drawable::vtable drawable::vrable_initializer<T>::vtable_ = {

drawable::vtable_initializer<T>::draw

};

保持するインスタンスのメンバ関数を呼び出すメンバ関数

33 of 57

仮想関数テーブルを用いた実装

このクラスは次のように利用します。

class Circle { void draw() { … } };

Circle circle;

Drawable drawable(circle);

drawable.draw();

34 of 57

仮想関数テーブルを用いた実装

このクラスは次のように利用します。

class Circle { void draw() { … } };

Circle circle;

Drawable drawable(circle);

drawable.draw();

ここで型消去が行われ、drawableに保持される

35 of 57

仮想関数テーブルを用いた実装

このクラスは次のように利用します。

class Circle { void draw() { … } };

Circle circle;

Drawable drawable(circle);

drawable.draw();

ここで型消去が行われ、drawableに保持される

独自実装の仮想関数テーブルによって

Circle::draw()が呼び出される

36 of 57

仮想関数テーブルを用いた実装

Drawableのコンストラクタはテンプレートになっています。

this_はvoid*で渡されたインスタンスへのポインタが、vptr_は後に解説する渡されたインスタンスの型ごとの仮想関数テーブルへのポインタが登録されます。このvptr_に登録される際に、型消去が行われています。

template <class T>

drawable(T& other) :

this_(&other),

vptr_(&vtable_initializer<T>::vtable_) {}

void* table_;

vtable* vptr_;

37 of 57

仮想関数テーブルを用いた実装

先ほど登録した仮想関数の仕組みは次のようになっています。

vtable_initializerはテンプレートクラスとなっており、内部は静的メンバ変数と静的メンバ関数になっています。型ごとに仮想関数テーブルとそれに対応するdraw()が生成される仕組みです。

struct vtable {

void (*draw)(void*);

};

template <class T>

struct vtable_initializer {

static vtable vtable_:

static void draw(void* this_) {

static_cast<T*>(this_)->draw();

}

};

38 of 57

仮想関数テーブルを用いた実装

各仮想関数テーブルはテンプレート初期化子リストによりdraw()と結びつけられます。

これで仮想関数テーブルの構築が完了しました。

template <class T>

drawable::vtable drawable::vrable_initializer<T>::vtable_ = {

drawable::vtable_initializer<T>::draw

};

39 of 57

仮想関数テーブルを用いた実装

実際にCircleのインスタンスが保持された状態でDrawable::draw()を呼び出すと、

vptr_の指す仮想関数テーブルからdraw()に保持しているインスタンスを渡し、呼び出します。

型消去されていたvoid*のthis_はここでテンプレートで指定されていた元の型に戻され、Circle::draw()呼び出す事が出来るのです。

void draw() {

vptr_->draw(this_);

}

static void vtable_initializer<Circle>::draw(void* this) {

static_cast<Circle*>(this_)->draw();

}

40 of 57

一般的な実装

次は主にBoostのようなライブラリでも採用されている手法について解説します。この実装には非テンプレート基底クラスと、派生テンプレートクラスを用いて型消去を行います。この実装もC++テンプレートテクニック第2版に掲載されています。

41 of 57

一般的な実装

採用しているライブラリ

・Boost.Any

・Boost.Function

・Boost.Shared_Ptr

などなど

42 of 57

一般的な実装

class Drawable {

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

Base* holder_;

public:

template <class T>

Drawable(const T& instance) :

holder_(new Delived<T>(instance)) {}

~Drawable() { delete holder_; }

void Draw() { holder_->Draw(); }

};

43 of 57

一般的な実装

class Drawable {

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

Base* holder_;

public:

template <class T>

Drawable(const T& instance) :

holder_(new Delived<T>(instance)) {}

~Drawable() { delete holder_; }

void Draw() { holder_->Draw(); }

};

仮想関数をもった基底クラス

44 of 57

一般的な実装

class Drawable {

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

Base* holder_;

public:

template <class T>

Drawable(const T& instance) :

holder_(new Delived<T>(instance)) {}

~Drawable() { delete holder_; }

void Draw() { holder_->Draw(); }

};

テンプレート派生クラス

(内部で型情報を持ったインスタンスを保持)

45 of 57

一般的な実装

class Drawable {

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

Base* holder_;

public:

template <class T>

Drawable(const T& instance) :

holder_(new Delived<T>(instance)) {}

~Drawable() { delete holder_; }

void Draw() { holder_->Draw(); }

};

初期化子で型消去を行うコンストラクタ

46 of 57

一般的な実装

仮想関数テーブルを用いる実装と同じ様に扱えます

class Circle { void draw() { … } };

Circle circle;

Drawable drawable(circle);

drawable.draw();

47 of 57

一般的な実装

Delivedクラスで型情報を持ったインスタンスを保持し、基底クラスで隠蔽する設計

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

class Drawable {

Base* holder_;

...

class Drawable {

...

template <class T>

Drawable(const T& instance) :

holder_(new Delived<T>(instance)) {}

...

48 of 57

一般的な実装

Delivedクラスで型情報を持ったインスタンスを保持し、基底クラスで隠蔽する設計

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

class Drawable {

Base* holder_;

...

class Drawable {

...

template <class T>

Drawable(const T& instance) :

holder_(new Delived<T>(instance)) {}

...

Base*で保持する

49 of 57

一般的な実装

struct Base {

virtual ~Base(){};

virtual void Draw() = 0;

};

template <class T>

struct Delived : public Base {

T instance_;

Delived(const T& instance) :

instance_(instance) {}

~Delived() {}

void Draw() override { instance_.Draw(); }

};

Draw()を呼び出すと動的ディスパッチによって、Derived::Draw()が呼び出される

class Circle { void draw() { … } };

Circle circle;

Drawable drawable(circle);

drawable.Draw();

50 of 57

Boost.TypeErasure

・boost 1.54から新しくBoost.TypeErasureが追加されました

・現行が1.55(8月上旬に1.56リリース予定)なので結構最近

・テンプレートで型消去に必要なコンセプトを定義できるので、非常に便利

51 of 57

Boost.TypeErasure

#include <boost/type_erasure/any.hpp>

#include <boost/type_erasure/member.hpp>

#include <boost/mpl/vector.hpp>

BOOST_TYPE_ERASURE_MEMBER((has_process), Process, 0)

typedef boost::type_erasure::any<

boost::mpl::vector<

boost::type_erasure::copy_constructible<>,

has_process<void(void)>

>

> process_holder_t;

52 of 57

Boost.TypeErasure

#include <boost/type_erasure/any.hpp>

#include <boost/type_erasure/member.hpp>

#include <boost/mpl/vector.hpp>

BOOST_TYPE_ERASURE_MEMBER((has_process), Process, 0)

typedef boost::type_erasure::any<

boost::mpl::vector<

boost::type_erasure::copy_constructible<>,

has_process<void(void)>

>

> process_holder_t;

53 of 57

Boost.TypeErasure

#include <boost/type_erasure/any.hpp>

#include <boost/type_erasure/member.hpp>

#include <boost/mpl/vector.hpp>

BOOST_TYPE_ERASURE_MEMBER((has_process), Process, 0)

typedef boost::type_erasure::any<

boost::mpl::vector<

boost::type_erasure::copy_constructible<>,

has_process<void(void)>

>

> process_holder_t;

Process()を保持していなければいけない

というコンセプトを定義

54 of 57

Boost.TypeErasure

struct Event

{

void Process() { std::cout << "event" << std::endl; }

};

int main() {

Event event;

process_holder_t process_holder(event);

process_holder.Process();

return 0;

}

独自実装のTypeErasureと同じように扱える

55 of 57

まとめ

TypeErasureはとても強力な機能ですが、独自実装の場合はコードの肥大化を招き、Boost.TypeErasureを利用する場合はmplのようなテンプレートライブラリをがんがん使っているため、コンパイルエラーが読みづらくなったりといった弊害があります。

ライブラリコードのみで利用するなど、用法容量をまもってご利用下さい。

56 of 57

おわり

ご清聴ありがとうございました。

57 of 57

参考文献

書籍:

『C++テンプレートテクニック第2版』 2014年 ISBN:978-4797376685

επιστημη (著), 高橋 晶 (著)

ストラウストラップのプログラミング入門』 2011年 ISBN:978-4797376685

ビャーネ・ストラウストラップ (著), Bjarne Stroustrup (著), επιστημη (監修), エピステーメー (監修), 遠藤 美代子 (株式会社クイープ) (翻訳)

規格書・リファレンス:

n3337 Working Draft, Standard for Programming Language C++

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

『BOOST 1.55.0 LIBRARY DOCUMENTATION』

http://www.boost.org/doc/libs/1_55_0/

参考サイト:

gununuの日記Boost.TypeErasureのドキュメントを翻訳してみた。

http://d.hatena.ne.jp/gununu/20130705/1372983790

『Cry’s Diary:TypeErasure - 型消去』

http://d.hatena.ne.jp/Cryolite/20051003

『cpprefjp』

https://sites.google.com/site/cpprefjp/