擬似仮想関数テンプレート
たまに仮想関数をテンプレート関数にしたくなることがあるが、C++ではできない。
#include <iostream> #include <typeinfo> namespace lib { class IDynamicType { public: virtual ~IDynamicType() = default; template<typename StaticType> virtual void function(const StaticType &value) = 0; // NG: テンプレート関数は仮想にできない }; } // namespace lib namespace app { class DynamicTypeA: public IDynamicType { public: template<typename StaticType> void function(const StaticType &value) override { // NG: テンプレート関数は仮想にできない std::cout << "DynamicTypeA::" << __func__ << "(" << typeid(value).name() << " " << value << ")\n"; } }; class DynamicTypeB: public IDynamicType { public: template<typename StaticType> void function(const StaticType &value) override { // NG: テンプレート関数は仮想にできない std::cout << "DynamicTypeB::" << __func__ << "(" << typeid(value).name() << " " << value << ")\n"; } }; } // namespace app
これは、非仮想のメンバーテンプレート関数と違って、新しい引数型を与えてfunction()が呼び出される度に、リンカーがIDynamicTypeクラスの仮想関数テーブルに対して新たなエントリーを追加しなければならなくなるからだ。
ただし、引数型を予め限定しても良いなら、クラステンプレートを使って擬似的に仮想関数テンプレートを作ることができる。
#include <algorithm> #include <cstddef> #include <memory> #include <string> namespace lib { template<typename... TPack> struct TypeList; template<> struct TypeList<> { static constexpr std::size_t size = 0; protected: ~TypeList() = default; }; template<typename THead, typename... TTail> struct TypeList<THead, TTail...>: TypeList<TTail...> { using head = THead; using tail = TypeList<TTail...>; static constexpr std::size_t size = sizeof...(TTail) + 1; protected: ~TypeList() = default; }; template<class TStaticTypeList, std::size_t t_size = TStaticTypeList::size> class DynamicType_IStaticType : public DynamicType_IStaticType<typename TStaticTypeList::tail> { protected: virtual ~DynamicType_IStaticType() = default; public: using DynamicType_IStaticType<typename TStaticTypeList::tail>::function; virtual void function(const typename TStaticTypeList::head &value) = 0; }; template<class TStaticTypeList> class DynamicType_IStaticType<TStaticTypeList, 1> { protected: virtual ~DynamicType_IStaticType() = default; public: virtual void function(const typename TStaticTypeList::head &value) = 0; }; template<class TStaticTypeList> class IDynamicType_IStaticTypeList : public DynamicType_IStaticType<TStaticTypeList> { }; using StaticTypeList = TypeList<int, std::string>; using IDynamicType = IDynamicType_IStaticTypeList<StaticTypeList>; class UseDynamicType { std::unique_ptr<IDynamicType> m_idynamic; public: explicit UseDynamicType(std::unique_ptr<IDynamicType> &&idynamic) : m_idynamic{std::move(idynamic)} {} public: void use_function() { m_idynamic->function(3); m_idynamic->function("xyz"); } }; } // namespace lib #include <iostream> #include <typeinfo> namespace app { template<class TStaticTypeList, std::size_t t_size = TStaticTypeList::size> class DynamicTypeA_StaticType : public DynamicTypeA_StaticType<typename TStaticTypeList::tail> { protected: virtual ~DynamicTypeA_StaticType() = default; public: using DynamicTypeA_StaticType<typename TStaticTypeList::tail>::function; void function(const typename TStaticTypeList::head &value) override { std::cout << "DynamicTypeA_StaticType::" << __func__ << "(" << typeid(value).name() << " " << value << ")\n"; } }; template<class TStaticTypeList> class DynamicTypeA_StaticType<TStaticTypeList, 0> : public lib::IDynamicType { protected: virtual ~DynamicTypeA_StaticType() = default; }; class DynamicTypeA: public DynamicTypeA_StaticType<lib::StaticTypeList> {}; template<class TStaticTypeList, std::size_t t_size = TStaticTypeList::size> class DynamicTypeB_StaticType : public DynamicTypeB_StaticType<typename TStaticTypeList::tail> { protected: virtual ~DynamicTypeB_StaticType() = default; public: using DynamicTypeB_StaticType<typename TStaticTypeList::tail>::function; void function(const typename TStaticTypeList::head &value) override { std::cout << "DynamicTypeB_StaticType::" << __func__ << "(" << typeid(value).name() << " " << value << ")\n"; } }; template<class TStaticTypeList> class DynamicTypeB_StaticType<TStaticTypeList, 0> : public lib::IDynamicType { protected: virtual ~DynamicTypeB_StaticType() = default; }; class DynamicTypeB: public DynamicTypeB_StaticType<lib::StaticTypeList> {}; } // namespace app
随分と複雑になってしまったが、仮想関数テンプレートであるかのように呼び出すことができる。
int main() { std::cout << "sizeof(lib::IDynamicType) = " << sizeof(lib::IDynamicType) << '\n'; { std::unique_ptr<app::DynamicTypeA> a{new app::DynamicTypeA}; std::cout << "sizeof(app::DynamicTypeA) = " << sizeof(app::DynamicTypeA) << '\n'; a->function(1); a->function("abc"); lib::UseDynamicType user{std::move(a)}; user.use_function(); } { std::unique_ptr<app::DynamicTypeB> b{new app::DynamicTypeB}; std::cout << "sizeof(app::DynamicTypeB) = " << sizeof(app::DynamicTypeB) << '\n'; b->function(2); b->function("def"); lib::UseDynamicType user{std::move(b)}; user.use_function(); } }
Clang 3.5.2/GCC 4.9.2でコンパイル~実行した結果は、以下のようになる。
sizeof(lib::IDynamicType) = 4 sizeof(app::DynamicTypeA) = 4 DynamicTypeA_StaticType::function(i 1) DynamicTypeA_StaticType::function(Ss abc) DynamicTypeA_StaticType::function(i 3) DynamicTypeA_StaticType::function(Ss xyz) sizeof(app::DynamicTypeB) = 4 DynamicTypeB_StaticType::function(i 2) DynamicTypeB_StaticType::function(Ss def) DynamicTypeB_StaticType::function(i 3) DynamicTypeB_StaticType::function(Ss xyz)
Visual C++ 2015 RCでコンパイル~実行した結果は、以下のようになる。
sizeof(lib::IDynamicType) = 4 sizeof(app::DynamicTypeA) = 4 DynamicTypeA_StaticType::function(int 1) DynamicTypeA_StaticType::function(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > abc) DynamicTypeA_StaticType::function(int 3) DynamicTypeA_StaticType::function(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > xyz) sizeof(app::DynamicTypeB) = 4 DynamicTypeB_StaticType::function(int 2) DynamicTypeB_StaticType::function(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > def) DynamicTypeB_StaticType::function(int 3) DynamicTypeB_StaticType::function(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > xyz)
クラス構成は、正確に表現できていないが以下の図のようになっている。
引数型が限定される上に、インターフェイス側も実装側も複雑なので、ここまでして実現したいことがあるかはわからないが、C++11の勉強がてら作ってみたので記録として残しておく。
参考
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る
- 作者: επιστημη,高橋晶
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2014/04/17
- メディア: 単行本
- この商品を含むブログ (6件) を見る
C++11:定数式の一般化とその保障:constexpr
C++11では、定数式を定義できるようになった。
enum class Flag: unsigned int { good = 1, fail = 2, bad = 4, eof = 8 }; constexpr Flag operator|(Flag x, Flag y) { return static_cast<Flag>( static_cast<unsigned int>(x) | static_cast<unsigned int>(y)); } void func(Flag f) { switch ( f ) { case Flag::bad: /* ... */ break; case Flag::eof: /* ... */ break; case Flag::bad|Flag::eof: /* ... */ break; default: /* ... */ break; } }
定数式には、コンパイル時に不明な値は利用できない。
int x1 = 7; constexpr int x2 = 7; constexpr int x3 = x1; // エラー:初期化子が定数式でない constexpr int x4 = x2; // OK
constexpr関数は、単純でなければならない。具体的には、return文は一つだけ許され、局所変数や選択文(if文など)や繰り返し文(for文など)は認められない。また、副作用を持ってもいけない。
ただし、条件式と再帰は使えるので、本当に必要であればあらゆるものをconstexpr関数にできる。(これらの制限の一部は、C++14で緩和された。)
constexpr int fact(int n) { return (n > 1) ? n * fact(n - 1) : 1; }
関数定義におけるconstexprは、コンパイル時に評価できることを示し、オブジェクト定義におけるconstexprは、初期化子をコンパイル時に評価する必要があることを示す。
なお、constexpr関数は、非定数の実引数を与えて呼び出すこともできるので、非定数用に同じ内容の関数を定義する必要はない。
void func(int n) { int f5 = fact(5); // コンパイル時に評価される可能性がある int fn = fact(n); // 実行時に評価される constexpr int f6 = fact(6); // コンパイル時に評価される constexpr int fnn = fact(n); // エラー:コンパイル時に評価できない }
constexprは、クラスのコンストラクターに付けることもできるが、コンストラクターの本体は空でなければならず、メンバー初期化子並びかオブジェクトの初期化子で全てのメンバー変数を初期化しなければならない。
struct Point { int x, y; constexpr Point(int xx = 0, int yy = 0): x{xx}, y{yy} {} }; constexpr Point origin; constexpr int ox{origin.x}; constexpr Point points[]{Point{1,2}, Point{3,4}, Point{5,6}}; constexpr int x{points[1].x};
constexprコンストラクターを持つクラスは、リテラル型と呼ばれる。
constexprは、全てのconstを置き換えるものではないことに注意しよう。
参考
- constant expressions (generalized and guaranteed;constexpr) (定数式 (一般化と保証付け; constexpr))
- C++14 constexpr関数の制限緩和 - Faith and Brave - C++で遊ぼう
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る
C++11:スコープ付きの強く型付けされた列挙体:enum class
C++11では、enum(従来の列挙体)の3つの問題を解決したenum classが導入された。
- 暗黙的にint型へ変換できるため、整数として振舞わせたくない場合にエラーの原因となる。
- 列挙子がスコープの外から見えるため、名前の衝突を引き起こす。
- 根底型(内部表現として利用される型)を指定できないため、混乱や互換性の問題の元となる。また、前方宣言できない。
enum classは、強く型付けされる。
enum Alert { red, blue }; // 従来の列挙体 enum class Color { red, blue }; // スコープ付きの強く型付けされた列挙体 Alert a1 = 7; // エラー:int型からの変換不可 Color c1 = 7; // エラー:int型からの変換不可 int a2 = red; // OK int c2 = Color::red; // エラー:int型への変換不可 Alert a3 = blue; // OK Color c3 = Color::blue; // OK
enum classの列挙子は、列挙体のスコープに入る。
enum Alert { red, blue }; enum class Color { red, blue }; enum Foo { red, blue }; // エラー:名前の衝突 enum class Bar { red, blue }; // OK Alert a1 = blue; // OK Color c1 = blue; // エラー Alert a2 = Alert::blue; // OK(C++98では不可) Color c2 = Color::blue; // OK
根底型を指定できるようになったことで、列挙体の相互運用性と大きさの保障が単純になった。
enum Alert { red, blue }; // 実装定義(C言語ではint) enum class Color: char { red, blue }; // 1バイト
根底型には汎整数型(論理型、文字型、整数型)を指定でき、デフォルトではint型となる。
前方宣言も可能になった。
// 前方宣言 enum Alert; // エラー enum class Color: char; // OK // 前方宣言の利用 void foo(Alert a); // エラー void bar(Color c); // OK // 定義 enum Alert { red, blue }; enum class Color: char { red, blue };
なお、C++11では、単なるenumでも根底型を指定でき、その場合は前方宣言もできる。
// 前方宣言 enum Alert: char; // OK // 前方宣言の利用 void foo(Alert a); // 定義 enum Alert: char { red, blue };
参考
- enum class (scoped and strongly typed enums) (enum class (スコープ付きの強く型付けされた enum))
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る
C++11:デフォルト関数の生成の制御:defaultおよびdelete
C++11では、「コピーを禁止する」という慣用句(イディオム)を直接表現できるようになった。
class Foo { Foo(const Foo &) = delete; Foo &operator=(const Foo &) = delete; // ... };
C++98では、private宣言して定義しないという手法がよく用いられた。
class Foo { private: Foo(const Foo &); Foo &operator=(const Foo &); // ... };
逆に、デフォルトの動作が望ましいなら、それを明示することもできる。
class Bar { // ... public: Bar() = default; ~Bar() = default; Bar(const Bar &) = delete; Bar &operator=(const Bar &) = delete; Bar(Bar &&) = default; Bar &operator=(Bar &&) = default; // ... };
=defaultは、デフォルトで生成されるどんな関数にでも使用でき、=deleteは、どんな関数にでも使用できる。
class D { double d; public: D(double); D(int) = delete; }; int main() { D d1(1.2); // OK D d2(3); // エラー }
デフォルトでは、クラスに対して以下の関数が生成される。
- デフォルトコンストラクター:X()
- コピーコンストラクター:X(const X &)
- コピー代入:X &operator=(const X &)
- ムーブコンストラクター:X(X &&)
- ムーブ代入:X &operator=(X &&)
- デストラクター:~X()
プログラマーがこれらの関数を宣言すると、コンパイラーは次の規則に基づいて生成しない。
- プログラマーがコンストラクターを一つでも宣言すれば、コンパイラーはデフォルトコンストラクターを生成しない。
- プログラマーがコピー関数、ムーブ関数、デストラクターの何れか一つでも宣言すれば、コンパイラーはコピー関数、ムーブ関数、デストラクターの何れも生成しない。
ただし、2つ目の規則は後方互換性維持のために不完全な効力しか持たず、私の環境(clang 3.4.2)ではBjarne Stroustrup氏の記事・書籍とも異なるよくわからないコンパイル結果となった。
そのため、コピー関数、ムーブ関数、デストラクターの何れか一つでも宣言する場合は、その他の関数も明示的に宣言した方が良いだろう。
参考
- control of defaults: default and delete(既定動作の制御: default と delete)
- control of defaults: move and copy(既定動作の制御: 移動とコピー)
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る
C++11:入れ子になったテンプレート引数
C++11では、入れ子になったテンプレート引数の閉じ括弧が連続する場合に空白が不要になった。
std::map<int, std::vector<std::string>> m;
C++98では、右シフト演算子や入力演算子と区別するために、空白が必要だった。
std::map<int, std::vector<std::string> > m;
参考
- right-angle brackets(不等号閉じ括弧)
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る
C++11:範囲for文
C++11では、ある範囲内の全要素の処理を簡潔に書けるようになった。
for ( auto x : 範囲 ) { // ... }
「範囲」は、std::vectorのように反復子を返すメンバー関数begin()/end()が定義されているクラスのオブジェクトでも良いし、組み込み配列のように非メンバー関数begin()/end()を適用できるオブジェクトでも良い。(メンバー関数の方が優先される。)
void func(std::vector<double> &v) { for ( auto x : v ) std::cout << x << "\n"; // 要素はxにコピーされる for ( auto &x : v ) ++x; // 要素を参照すると更新できる for ( const auto x : {1,2,5,13,34} ) std::cout << x << "\n"; }
int v[] = {1,2,5,13,34}; for ( auto x : v ) std::cout << x << "\n";
範囲for文を支援するために、標準ライブラリの<iterator>で範囲アクセス関数std::begin()/std::end()が定義されている。
namespace std { // クラス用 template<typename C> auto begin(C &c) -> decltype(c.begin()); template<typename C> auto begin(const C &c) -> decltype(c.begin()); template<typename C> auto end(C &c) -> decltype(c.end()); template<typename C> auto end(const C &c) -> decltype(c.end()); // 組み込み配列用 template<typename T, size_t N> auto begin(T (&array)[N]) -> T*; template<typename T, size_t N> auto end(T (&array)[N]) -> T*; }
つまり、冒頭の範囲for文は以下のコードと等価である。
for ( auto __begin = begin(範囲), __end = end(範囲); __begin != __end; ++__begin ) { auto x = *__begin; // ... }
範囲アクセス関数を直接使うことはあまりないだろうが、呼び出し方には注意が必要だ。
#include <algorithm> #include <iterator> template<typename Range, typename Function> void for_each_all(Range &range, Function func) { // 組み込み配列へ対応するために、using宣言する必要がある。 using std::begin; using std::end; // 非標準の範囲アクセス非メンバー関数へ対応するために、名前空間修飾無しで呼び出す必要がある。 auto first = begin(range); auto last = end(range); std::for_each(first, last, func); } #include <cstddef> struct MyContainer { static constexpr std::size_t m_a_size = 3u; int m_a[m_a_size]; MyContainer() : m_a{4, 5, 6} {} }; int *begin(MyContainer &c) { return c.m_a; } int *end(MyContainer &c) { return c.m_a + MyContainer::m_a_size; } #include <iostream> #include <vector> void print_line(int i) { std::cout << i << "\n"; } int main() { std::vector<int> v = {1, 2, 3}; MyContainer c; int a[] = {7, 8, 9}; for_each_all(v, print_line); for_each_all(c, print_line); for_each_all(a, print_line); }
(高橋氏の記事の「std名前空間のbegin()/end()をusing宣言しているのは、配列版を考慮するため。」という説明は、よくわからなかった。)
参考
- range-for statement(範囲 for 文)
- std::beginとstd::endの使い方 - Faith and Brave - C++で遊ぼう
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る
C++11:初期化子からの型の導出:auto
C++11では、「式」から値の型を導出することができるようになった。
auto x = 式;
初期化子から変数の型を導出できるauto型指定子は、型を書くことが面倒な場合や、正確に知ることが難しい場合に便利だろう。
template<class T> void print_all(const std::vector<T> &v) { for ( auto i = v.begin(); i != v.end(); ++i ) { std::cout << *i << "\n"; } }
C++98では、以下のように書く必要があった。
template<class T> void print_all(const std::vector<T> &v) { for ( typename std::vector<T>::const_iterator i = v.begin(); i != v.end(); ++i ) { std::cout << *i << "\n"; } }
型がテンプレート引数に強く依存している場合は、auto無しに書くことは難しいだろう。
template<class Tx, class Ty> auto multiply(const Tx &x, const Ty &y) -> decltype(x * y) { return x * y; }
(戻り値型の後方宣言やdecltypeについては、後日説明予定。)
導出される型に対して、constや&(参照)などの型指定子や修飾子を付けることもできる。
void func(std::vector<std::string> &v) { for ( const auto &s : v ) { // sはconst std::string & // ... } }
なお、autoの古い意味(「これはローカル変数である」)は、今では不正になった。
マイコーディングルール
- わざわざ型を明示すべき特別な事情が無い限り、autoを用いる。
- autoで変数を宣言する場合は、=構文を用いる。{}構文を用いると、std::initializer_list<T>型を導いてしまうため。(C++17で変更される)
auto x = 1; // xはint auto y{1}; // yはintではなくstd::initializer_list<int>(C++17ではint)
参考
- auto (type deduction from initializer) (auto (初期化子からの型導出))
- suffix return type syntax (extended function declaration syntax) (戻り値型の後方宣言 (関数宣言構文の拡張))
- decltype(decltype)
- C++17の新機能予定 - Faith and Brave - C++で遊ぼう
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/02/28
- メディア: 大型本
- この商品を含むブログ (8件) を見る