はじめに
大学の講義でEDSLが紹介され、「自分もC++でEDSLっぽいものを実装してみたい!」とモロに影響を受けてしまいました。
また、concept などのモダンな機能を、実際に使ってみたかったというのも重なって、勉強がてらHTMLもどきを生成するEDSLを作成してみました。
この記事は、完成したライブラリの紹介ではなく、concept や NTTP などの機能を使うとこんなことができる、という紹介や備忘録的なものです。特に実用性はないです。
今回は、「文法的に間違ったHTMLを書くと、コンパイル時にエラーになる」という型安全なHTMLビルダーのプロトタイプを作りながら、これらの機能がどう活用できるかを見ていきます。
完成品のイメージ
まずは、C++のコード上で関数をネストさせるようにHTMLを組み立てるインターフェースを目指します。変数も埋め込めるようにします。
int main() {
std::string user_name = "hogehoge";
// C++のコードとしてHTMLを組み立てる
auto my_page =
html(
body(
h1(text<"プロフィール">()),
ul(
li(text<"名前: ">(), var(user_name)),
li(text<"趣味: C++">())
)
)
);
// output.html に書き出し
std::ofstream file("output.html");
if (file.is_open()) {
my_page.render(file);
}
}
この記述方法の利点は、C++の型システムを利用して構造をチェックできることです。
例えば、ul の直下に li 以外のもの(直接テキストなど)を置こうとした場合、
auto my_page =
ul(
text<"liタグを忘れたテキスト">()
);
コンパイルエラーとして検出されます。
miss.cpp:110:9: error: no matching function for call to 'ul'
110 | ul(
| ^~
miss.cpp:71:6: note: candidate template ignored: constraints not satisfied [with Children = <Text<StringLiteral<33>{{108, 105, -29, -126, -65, -29, -126, -80, -29, -126, ...}}>>]
71 | auto ul(Children... children) { return UlElement<Children...>{ std::make_tuple(children...) }; }
| ^
miss.cpp:70:10: note: because 'Text<StringLiteral<33>{{108, 105, -29, -126, -65, -29, -126, -80, -29, -126, ...}}>' does not satisfy 'IsLiElement'
70 | template<IsLiElement... Children>
| ^
miss.cpp:54:44: note: because 'std::is_base_of_v<LiMarker, Text<StringLiteral<33>{{108, 105, -29, -126, -65, -29, -126, -80, -29, -126, -110, -27, -65, -104, -29, -126, -116, -29, -127, -97, -29, -125, -122, -29, -126, -83, -29, -126, -71, -29, -125, -120, 0}}> >' evaluated to false
54 | template<typename T> concept IsLiElement = std::is_base_of_v<LiMarker, T>;
| ^
1 error generated.
これを実現するために使用した機能と、実装のコア部分を3つに分けて解説します。
実装
1. 文字列を型として扱う
C++20から、テンプレート引数に文字列リテラルを直接渡せるようになりました(NTTP: Non-Type Template Parameter)。これを利用して、タグの名前や静的なテキストそのものを「型の一部」としてコンパイラに認識させます。
// 文字列リテラルを受け取るための構造体
template<size_t N>
struct StringLiteral {
char value[N];
constexpr StringLiteral(const char (&str)[N]) {
std::copy_n(str, N, value);
}
constexpr size_t size() const { return N - 1; }
};
// これを使って Text ノードを作る
template<StringLiteral Str>
struct Text {
void render(std::ostream& os) const {
os << std::string_view(Str.value, Str.size());
}
};
// 記述を短くするためのヘルパー
template<StringLiteral Str>
auto text() { return Text<Str>{}; }
これにより、Text<"Hello"> と Text<"World"> はコンパイラから見て全く別の型として扱われます。
2. タグの入れ子構造を作る
<div> や <ul> のような、中に子要素を持つタグを表現します。
子要素には Text や Var、あるいは別の Element など様々な型が入ってくるため、std::tuple を使って複数の型をまとめて保持します。
template<StringLiteral TagName, typename... Children>
struct Element {
std::tuple<Children...> children;
void render(std::ostream& os) const {
// 開始タグ
os << "<" << std::string_view(TagName.value, TagName.size()) << ">";
// std::apply と 畳み込み展開で、子要素のrenderを一気に呼ぶ
std::apply([&os](const auto&... child) {
(child.render(os), ...);
}, children);
// 終了タグ
os << "</" << std::string_view(TagName.value, TagName.size()) << ">\n";
}
};
可変長テンプレートと畳み込み展開(...)を活用することで、再帰的なツリー構造をシンプルに実装できます。
3. conceptによる構造の制約
「<ul> の中には <li> しか入れられない」というルールを、C++20 の concept を利用して実装します。
// ① 目印となるマーカーを用意
struct LiMarker {};
// ② concept の定義:「LiMarkerを継承している型だけを許可する」
template<typename T> concept IsLiElement = std::is_base_of_v<LiMarker, T>;
// ③ liタグの定義:Elementを継承しつつ、LiMarkerの目印をつける
template<typename... Children>
struct LiElement : Element<"li", Children...>, LiMarker {};
template<typename... Children>
auto li(Children... children) { return LiElement<Children...>{ std::make_tuple(children...) }; }
// ④ ulタグの定義:子要素(Children)の型を IsLiElement に制約する
template<IsLiElement... Children>
struct UlElement : Element<"ul", Children...> {};
template<IsLiElement... Children>
auto ul(Children... children) { return UlElement<Children...>{ std::make_tuple(children...) }; }
ポイントは④の template<IsLiElement... Children> という部分です。
従来の typename の代わりに concept(ここでは IsLiElement)を指定することで、「ここには特定のルールを満たした型しか渡せない」という制約をコンパイル時にかけることができます。これを利用してHTMLの文法をC++の型チェックに落とし込んでいます。
限界について
今回は「適当なEDSLを作る」「C++の機能を試す」という目的でプロトタイプを作成しましたが、これを実用的なHTMLライブラリとして完成させるのは相当厳しいです。
今回はul liなどのわかりやすい制約だけ扱いましたが、HTMLはもっと多くの細かいルールが存在します。それをconceptに落とし込んでいくのはとてもやってられないですよね。
属性なども実装しようとするとテンプレートの設計が複雑になりそうです。
あとそもそも実用性がそんなにない()
マイコンとかで簡単なページをホストするくらいしか使い道が思いつかないんですよね。
おわりに
C++の新しい機能を使って何か書いてみたいというときの題材としてEDSLの自作はとても良い練習になると思うので、興味がある方はぜひ試してみてください。