2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++でHTMLを生成してみる

2
Posted at

はじめに

大学の講義でEDSLが紹介され、「自分もC++でEDSLっぽいものを実装してみたい!」とモロに影響を受けてしまいました。
また、concept などのモダンな機能を、実際に使ってみたかったというのも重なって、勉強がてらHTMLもどきを生成するEDSLを作成してみました。

この記事は、完成したライブラリの紹介ではなく、conceptNTTP などの機能を使うとこんなことができる、という紹介や備忘録的なものです。特に実用性はないです。

今回は、「文法的に間違った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> のような、中に子要素を持つタグを表現します。
子要素には TextVar、あるいは別の 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の自作はとても良い練習になると思うので、興味がある方はぜひ試してみてください。

今回のコード
https://github.com/bubuzuke0950/cxx20-html-edsl

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?