C++20 Conceptに少し慣れようと思い、練習がてら雑JSONを出力できるライブラリを書いてみようと思った。
C++20となると色々考えるべきことが増える(std::u8string
をどうするか? とか)が、今回はそのへんは主題ではないのでできるだけ今までのC++の範囲内で短くまとめることにする。
コンセプトを使うとどれくらい単純化されるのか見てみよう。とりあえず、STLコンテナが来たときに、それが連想配列か普通の配列かを区別したい。手っ取り早く、key_type
とmapped_type
を持っているやつは連想配列ということにしてしまおう。
rangeライブラリが実装されていないのでrange
コンセプトを定義する必要がある。ライブラリの名前が何も思いつかなかったのでconceptからcptにした。
#include <concepts> namespace cpt { template<typename T> concept range = requires(T& t) { std::begin(t); // std::begin(t)が呼べる std::end (t); // 同上 }; template<typename T> concept has_value_type = requires { typename T::value_type; // T::value_type が存在する }; template<typename T> concept has_key_mapped_type = requires { typename T::key_type; typename T::mapped_type; }; template<typename T> concept map_like_container = range<T> && has_value_type<T> && has_key_mapped_type<T>; template<typename T> concept vector_like_container = range<T> && has_value_type<T> && !has_key_mapped_type<T>; } // cpt
これを使うと、std::vector
などの配列とstd::map
などの連想配列は以下のようにしてわけられる。
// 普通の配列をJSONにする template<vector_like_container T> inline std::string to_json(const T& v) { std::string retval; for(const auto& elem : v) { retval += ' '; retval += to_json(elem); retval += ','; } if(retval.empty()) {retval.resize(2);} retval.front() = '['; retval.back() = ']'; return retval; } // 連想配列をJSONにする template<map_like_container T> inline std::string to_json(const T& m) { std::string retval; for(const auto& [key, val] : m) { retval += ' '; retval += to_json(key); retval += ':'; retval += to_json(val); retval += ','; } if(retval.empty()) {retval.resize(2);} retval.front() = '{'; retval.back() = '}'; return retval; }
従来typename
だった部分をvector_like_container
とmap_like_container
にすればいいだけだ。驚きの簡単さ! 我々はSFINAEの意味のわからん記述から解放されるのだ!
整数や浮動小数点数のコンセプトは標準でサポートされている。
template<std::integral T> inline std::string to_json(const T i) { return std::to_string(i); } template<std::floating_point T> inline std::string to_json(const T f) { return std::to_string(f); }
あれ、分ける意味なかった……。
そんなこんなで一瞬で実装が終わる。さて、これにユーザー定義型を渡してみよう。ユーザー定義型がSTLコンテナと同じインターフェースを持っているとかでなければ、何にもマッチせずコンパイルエラーになるはずだ。さてどんなエラーになる?
struct X {int i;};
上記の構造体をto_json
に渡したコードを、我らがwandboxで実行してみよう。
prog.cc: In function 'int main()': prog.cc:113:36: error: no matching function for call to 'to_json(X)' 113 | std::cout << cpt::to_json(X{42}) << std::endl; | ^ prog.cc:41:20: note: candidate: 'std::string cpt::to_json(bool)' 41 | inline std::string to_json(const bool b) | ^~~~~~~ prog.cc:41:39: note: no known conversion for argument 1 from 'X' to 'bool' 41 | inline std::string to_json(const bool b) | ~~~~~~~~~~~^ prog.cc:48:20: note: candidate: 'std::string cpt::to_json(T) [with T = X; std::string = std::__cxx11::basic_string<char>]' 48 | inline std::string to_json(const T i) | ^~~~~~~ prog.cc:48:20: note: constraints not satisfied In file included from prog.cc:1: /opt/wandbox/gcc-head/include/c++/11.0.0/concepts: In instantiation of 'std::string cpt::to_json(T) [with T = X; std::string = std::__cxx11::basic_string<char>]': prog.cc:113:36: required from here /opt/wandbox/gcc-head/include/c++/11.0.0/concepts:102:13: required for the satisfaction of 'integral<T>' [with T = X] /opt/wandbox/gcc-head/include/c++/11.0.0/concepts:102:24: note: the expression 'is_integral_v<_Tp> [with _Tp = X]' evaluated to 'false' 102 | concept integral = is_integral_v<_Tp>; | ^~~~~~~~~~~~~~~~~~ prog.cc:54:20: note: candidate: 'std::string cpt::to_json(T) [with T = X; std::string = std::__cxx11::basic_string<char>]' 54 | inline std::string to_json(const T f) | ^~~~~~~ prog.cc:54:20: note: constraints not satisfied In file included from prog.cc:1: /opt/wandbox/gcc-head/include/c++/11.0.0/concepts: In instantiation of 'std::string cpt::to_json(T) [with T = X; std::string = std::__cxx11::basic_string<char>]': prog.cc:113:36: required from here /opt/wandbox/gcc-head/include/c++/11.0.0/concepts:111:13: required for the satisfaction of 'floating_point<T>' [with T = X] /opt/wandbox/gcc-head/include/c++/11.0.0/concepts:111:30: note: the expression 'is_floating_point_v<_Tp> [with _Tp = X]' evaluated to 'false' 111 | concept floating_point = is_floating_point_v<_Tp>; | ^~~~~~~~~~~~~~~~~~~~~~~~ prog.cc:59:20: note: candidate: 'std::string cpt::to_json(const string&)' 59 | inline std::string to_json(const std::string& s) | ^~~~~~~ prog.cc:59:47: note: no known conversion for argument 1 from 'X' to 'const string&' {aka 'const std::__cxx11::basic_string<char>&'} 59 | inline std::string to_json(const std::string& s) | ~~~~~~~~~~~~~~~~~~~^ prog.cc:66:20: note: candidate: 'std::string cpt::to_json(const T&) [with T = X; std::string = std::__cxx11::basic_string<char>]' 66 | inline std::string to_json(const T& v) | ^~~~~~~ prog.cc:66:20: note: constraints not satisfied prog.cc: In instantiation of 'std::string cpt::to_json(const T&) [with T = X; std::string = std::__cxx11::basic_string<char>]': prog.cc:113:36: required from here prog.cc:8:9: required for the satisfaction of 'range<T>' [with T = X] prog.cc:29:9: required for the satisfaction of 'vector_like_container<T>' [with T = X] prog.cc:8:17: in requirements with 'T& t' [with T = X] prog.cc:9:15: note: the required expression 'std::begin(t)' is invalid 9 | std::begin(t); | ~~~~~~~~~~^~~ prog.cc:10:15: note: the required expression 'std::end(t)' is invalid 10 | std::end (t); | ~~~~~~~~~~^~~ cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail prog.cc:82:20: note: candidate: 'std::string cpt::to_json(const T&) [with T = X; std::string = std::__cxx11::basic_string<char>]' 82 | inline std::string to_json(const T& m) | ^~~~~~~ prog.cc:82:20: note: constraints not satisfied prog.cc: In instantiation of 'std::string cpt::to_json(const T&) [with T = X; std::string = std::__cxx11::basic_string<char>]': prog.cc:113:36: required from here prog.cc:8:9: required for the satisfaction of 'range<T>' [with T = X] prog.cc:25:9: required for the satisfaction of 'map_like_container<T>' [with T = X] prog.cc:8:17: in requirements with 'T& t' [with T = X] prog.cc:9:15: note: the required expression 'std::begin(t)' is invalid 9 | std::begin(t); | ~~~~~~~~~~^~~ prog.cc:10:15: note: the required expression 'std::end(t)' is invalid 10 | std::end (t); | ~~~~~~~~~~^~~
やはりといえばそうなのだが、長い。
上から読んでいけば、「to_json(X{42})
にマッチする関数はありません」「to_json(const bool)
には以下の理由でマッチしません」「to_json(const T i)
には……」と書いてあるので簡単にエラーの理由がわかるのだけれど、慣れている人は一行目で理由を察するし、慣れていない人は長さに圧倒されて気を失う。いつもの光景だ。
さて、コンセプトは複雑怪奇なSFINAEを滅ぼすことでプログラマのコストを下げる、またライブラリのユーザーにしてもSFINAE由来の謎のクソ長いエラーメッセージが「このコンセプトにマッチしません」というメッセージになってWin-Win、という話が巷には溢れている。このエラーメッセージをもっと短くわかりやすくできないものか?
考えてみよう。まず、今回大量のエラーメッセージが出てきてしまうのは、複数の関数が潜在的にマッチできる状況にあるからだ。そのどれもがマッチしない理由をコンパイラは説明しなければならない。ではまず、マッチし得る関数が一つしかない状況を作ろう。とりあえず今までのto_json
関数を全部detail
名前空間に入れる。そして単一の関数to_json
をライブラリ名前空間のトップに置く。そしてその関数がdetail
名前空間内の関数に飛ばす、という流れを作ろう。
namespace cpt { namespace detail { inline std::string to_json(...); } template<typename T> inline std::string to_json(const T& v) { return detail::to_json(v); } }
これによって、cpt::to_json
の呼び出しはすべてこの関数に集約される。いわば門番になるわけだ。そうしたところで、このライブラリで出力できる型のすべてにマッチするようなコンセプトを書く。
これは、これまでオーバーロード解決に使ったコンセプトと、直接書いた型とsame_as
になるようなコンセプトをすべてor
で繋ぐと作れる。ちょっとダルいがこの程度なら許容範囲だ。
template<typename T> concept supported_types = std::same_as<T, bool> || std::integral<T> || std::floating_point<T> || std::same_as<T, std::string> || map_like_container<T> || vector_like_container<T> ; template<supported_types T> inline std::string to_json(const T& v) { return detail::to_json(v); }
これを使うことでエラーメッセージはどうなるか?
prog.cc: In function 'int main()': prog.cc:122:36: error: use of function 'std::string cpt::to_json(const T&) [with T = X; std::string = std::__cxx11::basic_string<char>]' with unsatisfied constraints 122 | std::cout << cpt::to_json(X{42}) << std::endl; | ^ prog.cc:103:20: note: declared here 103 | inline std::string to_json(const T& t) | ^~~~~~~ prog.cc:103:20: note: constraints not satisfied prog.cc: In instantiation of 'std::string cpt::to_json(const T&) [with T = X; std::string = std::__cxx11::basic_string<char>]': prog.cc:122:36: required from here prog.cc:33:9: required for the satisfaction of 'supported_types<T>' [with T = X] prog.cc:38:34: note: no operand of the disjunction is satisfied 34 | std::same_as<T, bool> || | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | std::integral<T> || | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | std::floating_point<T> || | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 37 | std::same_as<T, std::string> || | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 38 | map_like_container<T> || | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~ 39 | vector_like_container<T> ; | ~~~~~~~~~~~~~~~~~~~~~~~~ cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail
目論見通り、supported_types<T>
が満たされていないという表示だけが残った。さらにsupported_types
の内訳まで書いてくれている。
最初のエラーメッセージに比べてかなり単純でわかりやすくなったように思う。これは……実はいいアイデアでは? ただsupported_types
を定義するのがだるいという問題は残るけれど。