Edited at

C++に連想配列リテラルはない?→C++11ならあります。

More than 3 years have passed since last update.

自分は前々から知っていたが、「C++ 連想配列 リテラル」でぐぐっても出てこなかったので書きます。

自分がスクリプト言語を使っていてC++などに比べて楽だと思ったのが、連想配列リテラル。これがC++にないがために何かと面倒さを感じることも多かったのだが、C++11では(厳密にはリテラルではないし、連想配列専用のリテラル表記が導入されたわけではないものの)以下の方法でリテラルのごとく連想配列を書けるようになったのである。


1. 連想配列の中に連想配列が入るなどの構造がなくてよい場合(単純)

この場合はこんなコードになる。

ここではC++11で規格化されたstd::unordered_mapを使っているが、std::mapでも同じである。

#include <string>

#include <unordered_map>
#include <iostream>

int main(void){
std::unordered_map<std::string, int> m = {
{"foo", 3}, // キーが"foo"、それに対応する値が3。以下同様。
{"bar", 5},
{"buz", 10},
};

for(auto i = m.begin(); i != m.end(); ++i){
std::cout << i->first << " => " << i->second << std::endl;
}
}

出力は以下のようになる。(順番は環境による)

bar => 5

buz => 10
foo => 3


何をやってるの

上記のコードで、mに代入する部分が二重の波カッコになっているが、この外側と内側では意味合いが少々異なる(ただしどちらも、規格化されたのはC++11にてである)。

m = { // 外側

{"foo", 3}, // 内側
{"bar", 5}, // 内側
{"buz", 10}, // 内側
}; // 外側

まず外側は「initializer_list」といって、これは従来{1, 2, 3}のようなコードで要素を初期化できるのが配列だけであった(例えば、int foo[] = {1, 2, 3})ものを、他のクラスにおいても使えるようにしたものである。これがstd::mapstd::unordered_mapでは、カンマで区切られた各要素が連想配列の一つの要素になる、というものである。

また内側については、「コンストラクタにその引数を渡す」ということの略記が波カッコでできるようになったので、それを利用しているのである。本来であればこの部分は

m = { // 外側

std::pair<std::string, int>("foo", 3), // 内側
std::pair<std::string, int>("bar", 5), // 内側
std::pair<std::string, int>("buz", 10), // 内側
}; // 外側

と書かないとならないのが、型名を省略して書いている(std::unordered_mapの引数の型から決まるので省略できる)というものである。


2. 連想配列の中に連想配列を入れたい場合(面倒)

これはリテラルがあるかというより、型付けがある言語ゆえの制限なのだが、

{"foo": {"bar": 1, "buz": 3},

"hoge": 10}

のような連想配列(JSON表記)は、C++標準の連想配列型に割り当てることはできない。キーの型も値の型もそれぞれ一つしか指定できないためである(上記の例では、"foo"に対応する値が連想配列なのに対し、"buz"に対応する値は整数)。

ただしこの連想配列の解釈を変えて、以下のような「各連想配列の値が、本来持ちたい整数値と、子要素の集合の組になっている」ようにすれば作ることができる(中間ノードにも値がある木構造のようなもの)。なお、上記の例ではfooに対応する整数値がなかったのに対し、下記の例では-1を割り当てている。

{

"foo": {
"value": -1,
"child": {
"bar": {
"value": 1,
"child": {}
},
"buz": {
"value": 3,
"child": {}
}
},
"hoge": {
"value": 10,
"child": {}
}
}


【追記・補足】C++では型付けがあるとはいえ、「任意の型の値を表現できる型」や「複数の型の値のどれかを表現する型」などを構築する方法はあるため、値として整数や連想配列などが混ざっていても格納できるものも(単純ではないですが)作れます(@ignis_fatuusさんから情報をいただきました)。

サンプルコード(json j2 = {... の部分)にある通り、JSONで表現できるものが何でも表現できます。


実際、この後に載せるコードを利用すれば、上記の連想配列に相当するものを、C++のコードとして

m = {

{"foo", -1, {{"bar", 1}, {"buz", 3}} },
{"hoge", 10},
};

のようにかける。これの実装例は以下の通り。

#include <iostream>

#include <map>
#include <string>
#include <initializer_list>

template <class KEYTYPE, class VALUETYPE> class MultiLayerMap;

// 実装上は、mapのキーに実際のキー、mapの値に「実際の値と、子要素を表す連想配列の組」を
// 割り当てないとならないため、その後者を定義したのがMultiLayerMapNodeである。
template <class KEYTYPE, class VALUETYPE>
struct MultiLayerMapNode{
// 値
VALUETYPE value;

// 子要素
MultiLayerMap<KEYTYPE, VALUETYPE> children;

MultiLayerMapNode(){} // これがないとコンパイルが通らなかった
MultiLayerMapNode(const MultiLayerMapNode<KEYTYPE, VALUETYPE> & other) : value(other.value), children(other.children) {}
};

// これは、次に定義するMultiLayerMapにおいて、
// initializer_list<???> の ??? の部分として指定する型である。
//
// そもそもinitializer_listを使う場合、同じ型が並んだものでないとならない。
// 通常の連想配列であれば、initializer_listの各要素がstd::pair<キー, 値>だったのだが、
// 今回はキー・値・子要素の3つが必要なのでこのクラスを定義している。
//
// また、子要素がない場合は省略できるようにしたいため、コンストラクタが
// - 引数2つ:{キー, 値} とみなす
// - 引数3つ:{キー, 値, 子要素} とみなす
// ようにオーバーロードしている。
template <class KEYTYPE, class VALUETYPE>
struct MultiLayerMapElement{
KEYTYPE key;
MultiLayerMapNode<KEYTYPE, VALUETYPE> node;

// 子要素がない場合
MultiLayerMapElement(const KEYTYPE & k, const VALUETYPE & v){
key = k;
node.value = v;
}

// 子要素がある場合
MultiLayerMapElement(const KEYTYPE & k, const VALUETYPE & v, const MultiLayerMap<KEYTYPE, VALUETYPE> & c){
key = k;
node.value = v;
node.children = c;
}
};

// 実際の連想配列の型。std::mapからの継承によって定義している。
template <class KEYTYPE, class VALUETYPE>
class MultiLayerMap : public std::map<KEYTYPE, MultiLayerMapNode<KEYTYPE, VALUETYPE>>{
public:
MultiLayerMap(){} // これがないとコンパイルが通らなかった

MultiLayerMap(std::initializer_list<MultiLayerMapElement<KEYTYPE, VALUETYPE>> ls){
for(auto i = ls.begin(); i != ls.end(); ++i){
(*this)[i->key] = i->node;
}
}
};

// 出力。階層が何階層あるかわからないため、再帰にしている
template <class KEYTYPE, class VALUETYPE>
void outputMultiLayerMap(const MultiLayerMap<KEYTYPE, VALUETYPE> & m, size_t level){
for(auto i = m.begin(); i != m.end(); ++i){
for(size_t j = 0; j < level; ++j) std::cout << " ";
std::cout << i->first << " => " << i->second.value << std::endl;
outputMultiLayerMap(i->second.children, level+1);
}
}

template <class KEYTYPE, class VALUETYPE>
void outputMultiLayerMap(const MultiLayerMap<KEYTYPE, VALUETYPE> & m){
outputMultiLayerMap(m, 0);
}

// メインパート
int main(){
// JSONでいうところの
// {"foo": {"bar": 1, "buz": 3},
// "hoge": 10}
// のようなものを定義している。
// ただし、"foo"に対するvalueを空にできないので、-1を指定している。
MultiLayerMap<std::string, int> m = {
{"foo", -1, {
{"bar", 1},
{"buz", 3},
}},
{"hoge", 10},
};

// 出力
outputMultiLayerMap(m);
}

これを実行すると、以下のようになる。(gcc4.7.2ならびにVisual Studio 2015で確認)

foo => -1

bar => 1
buz => 3
hoge => 10