unorderd_map のキーに enum 型を使用する このエントリーをはてなブックマークに追加

新年明けましておめでとうございます。去年はちっとも儲からなかったので、今年は本腰入れて開発やって自力で稼げる事業を立ち上げたく、その準備を進めて参る所存でございます。どうぞ生暖かく見守っていただければと思います…。

さて、前回の記事でお見せした、 iconv のラッパークラスをテンプレートクラスに作り直す際、 <unorderd_map> を利用していて気づいたことの備忘録です。

問題

テンプレート引数に、内部文字列の (アラインメントを決定する) 物理型と文字セットを指定できるようにしたかったのですが、文字セットを示す文字列そのものをテンプレート引数に渡すことはできないので、代わりに文字セットを表す列挙値を定義し、対応する「文字セットを表す文字列」と関連づけた連想配列を <unorderd_map> を用いて用意する、ということをやってみることにしました。 C/C++ の列挙値は int にキャスト可能なので昔の自分だったら単純に「文字セットを表す文字列」の配列を用意して添え字代わりに列挙値を突っ込むところですが、メンテナンス性良くないですし、ハッシュテーブルなら検索コストは (ほぼ) 変わらないですからね。

で、ヘッダファイルの方で列挙型を定義しーの、

enum charset_t {
    chset_Utf8, chset_C99, chset_Java,
    chset_Ucs2, chset_Ucs2Be, chset_Ucs2Le,
    chset_Ucs4, chset_Ucs4Be, chset_Ucs4Le,
    chset_Utf16, chset_Utf16Be, chset_Utf16Le,
    chset_Utf32, chset_Utf32Be, chset_Utf32Le,
    chset_Utf7,
    chset_EucJp, chset_EucJis0213,
    chset_Iso2022Jp, chset_Iso2022Jp2, chset_Iso2022Jp1, chset_Iso2022Jp3,
    chset_ShiftJis, chset_Cp932, chset_ShiftJisX0213,
};

テンプレート化しない実装部分のクラスに staticconst な連想配列メンバを追加しーの、

class EncodeStringImpl
{
    typedef std::unordered_map<charset_t, char const *> cnmap_t;
    static cnmap_t const CharsetNames;

    // ...
};

実装ファイルの方で連想配列の値を定義しーの、

EncodeStringImpl::cnmap_t const EncodeStringImpl::CharsetNames = {
    { chset_Utf8, "UTF-8" }, { chset_C99, "C99" }, { chset_Java, "JAVA" },
    { chset_Ucs2, "UCS-2" }, { chset_Ucs2Be, "UCS-2BE" }, { chset_Ucs2Le, "UCS-2LE" },
    { chset_Ucs4, "UCS-4" }, { chset_Ucs4Be, "UCS-4BE" }, { chset_Ucs4Le, "UCS-4LE" },
    { chset_Utf16, "UTF-16" }, { chset_Utf16Be, "UTF-16BE" }, { chset_Utf16Le, "UTF-16LE" },
    { chset_Utf32, "UTF-32" }, { chset_Utf32Be, "UTF-32BE" }, { chset_Utf32Le, "UTF-32LE" },
    { chset_Utf7, "UTF-7" },
    { chset_EucJp, "EUC-JP" }, { chset_EucJis0213, "EUC-JISX0213" },
    { chset_Iso2022Jp, "ISO-2022-JP" }, { chset_Iso2022Jp2, "ISO-2022-JP2" },
    { chset_Iso2022Jp1, "ISO-2022-JP1" }, { chset_Iso2022Jp3, "ISO-2022-JP3" },
    { chset_ShiftJis, "SHIFT_JIS" }, { chset_Cp932, "CP932" }, { chset_ShiftJisX0213, "SHIFT_JISX0213" },
};

実際に使いーの、とやってみたはよいのですが、

void EncodeStringImpl::encode(void const *src, size_t src_length, size_t chr_size, charset_t from_charset,
    charset_t to_charset)
{

    // ...

    class auto_iconv_t {    // 生成時に iconv_open してスコープ抜ける時に iconv_close するプライベートクラス…
        const iconv_t impl;
    public:
        auto_iconv_t(charset_t from_cs, charset_t to_cs) :
            // ↓こことか
            impl(iconv_open(CharsetNames.find(to_cs)->second, CharsetNames.find(from_cs)->second))
        {
            if (impl == reinterpret_cast<iconv_t>(-1)) {
                // ↓こことか
                throw EncodeStringException(string("LIBICONV initialize error: please check character set name \"") +
                    CharsetNames.find(from_cs)->second + "\"(from) or \"" + CharsetNames.find(to_cs)->second +
                    "\"(to)");
            }
        }
        ~auto_iconv_t() { iconv_close(impl); }
        iconv_t get() const { return impl; }
    } iconv_handle(from_charset, to_charset);

いざ g++ してみると、「std::hash<charset_t>::operator()(charset_t) const なんて定義されてねーよ」とリンカ様に怒られてしまいました。

murachi@ubuntu-vbox:~/otoco/trunk/sample$ g++ -std=c++0x -o kumapan EncodeString.cpp kumapan.cpp 
/tmp/ccIn30PX.o: In function `std::__detail::_Hash_code_base<charset_t, std::pair<charset_t const, char const*>, std::_Select1st<std::pair<charset_t const, char const*> >, std::equal_to<charset_t>, std::hash<charset_t>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, false>::_M_hash_code(charset_t const&) const':
EncodeString.cpp:(.text._ZNKSt8__detail15_Hash_code_baseI9charset_tSt4pairIKS1_PKcESt10_Select1stIS6_ESt8equal_toIS1_ESt4hashIS1_ENS_18_Mod_range_hashingENS_20_Default_ranged_hashELb0EE12_M_hash_codeERS3_[std::__detail::_Hash_code_base<charset_t, std::pair<charset_t const, char const*>, std::_Select1st<std::pair<charset_t const, char const*> >, std::equal_to<charset_t>, std::hash<charset_t>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, false>::_M_hash_code(charset_t const&) const]+0x19): undefined reference to `std::hash<charset_t>::operator()(charset_t) const'
/tmp/ccIn30PX.o: In function `std::__detail::_Hash_code_base<charset_t, std::pair<charset_t const, char const*>, std::_Select1st<std::pair<charset_t const, char const*> >, std::equal_to<charset_t>, std::hash<charset_t>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, false>::_M_bucket_index(std::__detail::_Hash_node<std::pair<charset_t const, char const*>, false> const*, unsigned int) const':
EncodeString.cpp:(.text._ZNKSt8__detail15_Hash_code_baseI9charset_tSt4pairIKS1_PKcESt10_Select1stIS6_ESt8equal_toIS1_ESt4hashIS1_ENS_18_Mod_range_hashingENS_20_Default_ranged_hashELb0EE15_M_bucket_indexEPKNS_10_Hash_nodeIS6_Lb0EEEj[std::__detail::_Hash_code_base<charset_t, std::pair<charset_t const, char const*>, std::_Select1st<std::pair<charset_t const, char const*> >, std::equal_to<charset_t>, std::hash<charset_t>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, false>::_M_bucket_index(std::__detail::_Hash_node<std::pair<charset_t const, char const*>, false> const*, unsigned int) const]+0x28): undefined reference to `std::hash<charset_t>::operator()(charset_t) const'
collect2: ld はステータス 1 で終了しました
murachi@ubuntu-vbox:~/otoco/trunk/sample$ 

解決策

ヘッダーファイルの方で、以下の記述を列挙型の定義のすぐ後辺りに追加してやることで、リンクも通るようになります。テンプレートの特殊化ってやつですね。

namespace std {
template <>
    inline size_t
    hash<charset_t>::operator()(charset_t val) const
{
    return static_cast<size_t>(val);
}
}

解説

std::hash テンプレートクラスの実装は、 libstdc++ の場合、ヘッダファイルが置かれる然るべきディレクトリ配下の bits/functional_hash.h に記述されています。ここでは、非ポインタ型 T に対する operator() は (宣言はされているものの) 定義されていません。

  /// Primary class template hash.
  template<typename _Tp>
    struct hash : public __hash_base<size_t, _Tp>
    {
      size_t
      operator()(_Tp __val) const;
    };

  /// Partial specializations for pointer types.
  template<typename _Tp>
    struct hash<_Tp*> : public __hash_base<size_t, _Tp*>
    {
      size_t
      operator()(_Tp* __p) const
      { return reinterpret_cast<size_t>(__p); }
    };

で、この operator() の実装は、ごくごく基本的な組み込み型に対してのみ特殊化されています (ここは流石に長くなるので引用しませんが…)。

こうした実装は C++11 のドラフトにも明記されている標準の仕様に則ったものです。「20.8.12 Class template hash」に以下のような記述があります。

  1. The unordered associative containers defined in 23.5 use specializations of the class template hash as the default hash function. For all object types Key for which there exists a specialization hash<Key>, the instantiation hash<Key> shall:
    • satisfy the Hash requirements (17.6.3.4), with Key as the function call argument type, the DefaultConstructible requirements (Table 19), the CopyAssignable requirements (Table 23),
    • be swappable (17.6.3.2) for lvalues,
    • provide two nested types result_type and argument_type which shall be synonyms for size_t and Key, respectively,
    • satisfy the requirement that if k1 == k2 is true, h(k1) == h(k2) is also true, where h is an object of type hash<Key> and k1 and k2 are objects of type Key.
    template <> struct hash<bool>;
    template <> struct hash<char>;
    template <> struct hash<signed char>;
    template <> struct hash<unsigned char>;
    template <> struct hash<char16_t>;
    template <> struct hash<char32_t>;
    template <> struct hash<wchar_t>;
    template <> struct hash<short>;
    template <> struct hash<unsigned short>;
    template <> struct hash<int>;
    template <> struct hash<unsigned int>;
    template <> struct hash<long>;
    template <> struct hash<unsigned long>;
    template <> struct hash<long long>;
    template <> struct hash<unsigned long long>;
    template <> struct hash<float>;
    template <> struct hash<double>;
    template <> struct hash<long double>;
    template <class T> struct hash<T*>;
    

そんな訳で、デフォルトで std::hash を利用する std::unordered_mapenum 型をキーにして使用したい場合には、その enum 型で std::hash::operator() を特殊化してあげる必要があるのです。

2012 年 1 月 2 日 by 村山 俊之

タグ: , , ,

コメントをどうぞ