10月の論文集が発表されるまで暇なので、typeidとtype_infoと、C++11から追加されたtype_indexの解説をする。
C++でRTTI(Run Time Type Infomation/Identification)と呼ばれる機能がある。
std::type_infoは、型情報を表現するクラスである。type_infoのオブジェクトは、typeid式で得ることができる。typeid式は、オペランドに式かtype-idを取る。typeid式の結果は、constなstd::type_info型のlvalueである。typeid式を使うには、typeinfoヘッダーをincludeする必要がある。
#include <typeinfo>
std::type_info const & t1 = typeid(int) ;
std::type_info const & t2 = typeid(double) ;
std::type_infoはデフォルト構築もコピーもムーブもできないクラスである。std::type_info型のオブジェクトは、typeid式によってしか作れない。
typeid式は、constなtype_info型のlvalueを返すので、type_infoを使う場合は、リファレンスかポインターで参照しなければならない。
std::type_info const & t1 = typeid(int) ;
std::type_info const * t2 = &typeid(int) ;
typeid式が返すオブジェクトの寿命は、通常とは変わっていて、プログラムが終了するまでである。つまり、以下のようなコードが動く。
#include <typeinfo>
int main()
{
std::typeinfo const * ptr ;
{
ptr = &typeid(int) ;
}
ptr->name() ; // OK、
}
typeidが返すオブジェクトの寿命はプログラムの終了までなので、リファレンスやポインターで安全に保持できる。寿命や解放を考える必要はない。
type_infoのオブジェクトは型を表現する。type_infoのオブジェクト同士を等号比較することで、型が等しいかどうかを調べられる。
typeid(int) == typeid(double) ; // false
また、nameというメンバー関数を持っていて、これは実装依存の型を表現する何らかのnull終端された文字列を返す。
int main()
{
std::cout << typeid(int).name() << '\n' ;
}
GNU/Linuxでは、IntelのItanium用のC++ ABIがデファクトスタンダードとなっていて、GNU/Linux用のGCCやClangでは、この仕様に従ったマングル名が返される。デマングルして人間にわかりやすい型にするには、以下のようにすればよい。
#include <cxxabi.h>
#include <cstdlib>
#include <string>
#include <memory>
struct free_delete
{
template < typename T >
void operator ()( T * ptr ) const noexcept
{
std::free( ptr ) ;
}
} ;
std::string demangle( std::type_info const & ti )
{
int status = 0 ;
std::unique_ptr<char, free_delete >
ptr( abi::__cxa_demangle( ti.name(), nullptr, nullptr, &status ) ) ;
if ( !ptr )
{
switch ( status )
{
case -1 :
return "memory allocation failure" ;
case -2 :
return "invalid mangled name" ;
case -3 :
return "invalid arguments" ;
default :
return "Shouldn't reach here" ;
}
}
std::string result( ptr.get() ) ;
return result ;
}
それはさておき・・・
typeid式が実行時型情報と呼ばれている理由は、ポリモーフィッククラス型の式をオペランドに与えると、実行時になるまで分からない型を表現するtype_infoが返されるからだ。
struct A
{
virtual void polymorphic() { }
} ;
struct B : A
{ } ;
void f( A & ref )
{
// refがA, Bどちらを参照するのかは実行時に決まる
// tiが表現する型も実行時に決まる
decltype(auto) ti = typeid( ref ) ;
}
これにより、以下のようなコードを書くことができる。
struct Base { virtual void p() = 0 ; } ;
struct Cat : Base { } ;
struct Dog : Base { } ;
std::string get_name( Base & ref )
{
decltype(auto) ti = typeid(ref) ;
if ( ti == typeid(Cat) )
return u8"猫" ;
else if ( ti == typeid(Dog) )
return u8"犬" ;
return u8"謎" ;
}
もちろん、世間一般的に、この場合にはvirtual関数を使うのが礼儀正しい作法であるとされている。
struct Base { virtual std::string get_name() = 0 ; } ;
struct Cat : Base { std::string get_name() { return u8"猫" ; } } ;
struct Dog : Base { std::string get_name() { return u8"犬" ; } } ;
auto get_name( Base & ref )
{
return ref.get_name() ;
}
さて、typeidの結果であるtype_info型は、かなり扱いづらい。すでに説明したように、type_infoはデフォルト構築、コピー、ムーブができず、typeid式から得られるオブジェクトを、リファレンスやポインター経由で参照して使うしかないからだ。これは、type_info型のオブジェクトの大量に管理したい場合に問題になる。オブジェクトを大量に管理するには、vectorやmapやunordered_mapなどのコンテナーを使いたいが、type_info型を直接使うわけには行かない。とすると、ポインターだろうか。
std::vector< std::type_info * > v ;
vectorならこれでよくても、やはり型情報という性質上、それぞれの型に何らかの値を付属させて、後から検索したい都合も出てくる。mapやunordered_mapで使いたい。
そのためには、type_info *型をラップするという手がある。しかし、正しくラップするのは、以下のように単調でめんどくさい。
namespace ezoe {
class type_index
{
public:
type_index(const std::type_info& rhs) noexcept
: target( &rhs ) { }
bool operator==(const type_index& rhs) const noexcept
{ return *target == *rhs.target ; }
bool operator!=(const type_index& rhs) const noexcept
{ return *target != *rhs.target ; }
bool operator< (const type_index& rhs) const noexcept
{ return target->before( *rhs.target ) ; }
bool operator<= (const type_index& rhs) const noexcept
{ return !target->before( *rhs.target ) ; }
bool operator> (const type_index& rhs) const noexcept
{ return rhs.target->before( *target ) ; }
bool operator>= (const type_index& rhs) const noexcept
{ return !rhs.target->before( *target) ; }
std::size_t hash_code() const noexcept
{ return target->hash_code() ; }
const char* name() const noexcept
{ return target->name() ; }
private:
const std::type_info* target;
} ;
}
namespace std
{
template < >
struct hash< ezoe::type_index >
{
size_t operator() ( ezoe::type_index ti ) const noexcept
{
return ti.hash_code() ;
}
} ;
}
このコードは、誰が書いてもこのようになる。しかし、これを正しく書くのは面倒で、間違いやすく、しかもお互いに非互換なラッパーが乱立してしまう。そこで、このようなラッパー、std::type_indexが、C++11では標準ライブラリに追加された。使うには、ヘッダーファイルtypeindexをincludeする必要がある。
#include <typeindex>
#include <map>
int main()
{
std::map< std::type_index, std::string > m =
{
{ typeid(int), "int" },
{ typeid(double), "double"}
} ;
std::cout << m[typeid(int)] << '\n' ;
}
ドワンゴ広告
この記事はドワンゴ勤務中に書かれた。
次に解説すべきC++11で追加されたライブラリを探している。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0
3 comments:
demangleが例外安全ではない(resultのコンストラクタでthrowされると*ptrがリークする)
もうちょっと簡単なRAIIラッパーが標準ライブラリに欲しいものだ。
興味深く拝見させてもらいました。
こういう型情報を使ったプログラムってあんまりしないのですが、何かに使えますかね。
うーん。
Post a Comment