2014-10-08

C++のtypeidとtype_infoとtype_index

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:

Anonymous said...

demangleが例外安全ではない(resultのコンストラクタでthrowされると*ptrがリークする)

江添亮 said...

もうちょっと簡単なRAIIラッパーが標準ライブラリに欲しいものだ。

Anonymous said...

興味深く拝見させてもらいました。
こういう型情報を使ったプログラムってあんまりしないのですが、何かに使えますかね。
うーん。