x64セルフホスティングCコンパイラを書いた

前回:

ryooooooga.hateblo.jp

はじめに

セルフホスト(自分自身をビルド)できるCコンパイラmoccを書きました。

github.com

前回はバックエンドをLLVMに頼っていたため、今回はx86_64向けのアセンブリを吐くようにしました。 基本的な設計はほぼ前回と同じですが、コードは全て新しく書き直しました。

例によって@rui314さんの低レイヤを知りたい人のためのCコンパイラ作成入門を参考にしています。

工夫した点

基本的な点は、前回とほぼ同じなので上記の記事を参照してください。

前回に比べ、拡張性や規格準拠性を意識して構文木の構造を一部変更しましたが、 結局セルフホストに必要な最小限の機能のみを実装したため、使える言語機能は下記を除けば前回のものと大差ありません。

新しく使えるようにした機能:

  • 列挙体
  • 変数の宣言時初期化
  • 複数の変数の同時宣言

GitHub Actionsを用いたテストの自動化

前回は面倒がっていたテストの自動化を行いました。

GitHub Actionsはお手軽でいいですね。

まとめ

前回のCコンパイラはバックエンドをLLVMに頼っていたため、 x86_64向けのアセンブリを吐くCコンパイラを作成しました。

出力コードの効率などは度外視していたためひどく非効率的なコードが出力されますが、 バックエンドを含め実装することができました。

出力コードの最適化は今後の課題です。

Almel v1.1.3 をリリースした

ryooooooga.hateblo.jp

自作shellテーマ、Almel を更新しました。

v1.0.xとの変更点は以下の通りです。

  • コードを全部書き直した
  • コンフィグファイルの構造を変えた
  • fish, bashに対応した
  • 現在時刻を表示する time セグメントを追加
  • 直前に実行されたコマンドの実行時間を表示する duration セグメントの追加
  • シェルごとに異なる内容を表示できる shell セグメントを追加

コンフィグファイル (~/.config/almel/almel.yaml) に互換性がないため、エラーが発生する場合には一度コンフィグファイルを削除してください。

コードの全リライトの副作用として若干実行速度がはやくなりました。

また、GitHub Actionsを使って x86_64 環境向けのバイナリ (Windows, mac, Linux) を自動的に用意するようにしました。

v1.1.3 のデフォルトの見た目は次のようになります。

f:id:Ryooooooga:20191107092344p:plain
デフォルトの見た目

全部盛りのため結構うるさいです。 コンフィグファイルの segments 項目で表示するセグメントとその表示順を変更できます。

以下に自分用の設定を晒します。

os:
  linux:
    background: 33
  mac:
    background: 33
  windows:
    background: 33

git_user:
  background: 75

time:
  format: "\uf017 %H:%M:%S"

duration:
  background: 242

segments:
  - - os
    - user
    - directory
    - git_repo
    - git_user
  - - duration
    - status

f:id:Ryooooooga:20191107092355p:plain
上記設定時の見た目

zshのプロンプトテーマを自作した

自分用にzshのプロンプトテーマをRustで自作しました。 自分が常用しても問題ない程度の完成度になったので公開しました。

github.com

f:id:Ryooooooga:20191021210953p:plain
使用時の様子

セグメントの表示内容は左上から

  • OS
  • ユーザ名@ホスト名
  • (中間省略された) カレントディレクト
  • Gitブランチ
  • Gitユーザ
  • 直前の終了ステータス

となっています。(順番、色、表示/非表示などに関してはカスタマイズ可能)

まだREADMEが空っぽなので後で中身を書きます。

以前使用していたプロンプト

bobthefish

ログインシェルにfishを使用していた頃 (~ 2018年9月) はプロンプトテーマに bobthefish を使用していました。

github.com

高機能で、更に環境変数によってある程度のカスタマイズができる非常によいテーマでした。 特に、それまではデフォルトプロンプトのbashやfishしか使ってこなかったこともあり、Powerline系のセグメントやGit statusの表示などは一種のカルチャーショックでした。 なにより見た目がかっこいい。テンションがあがるものはよいものにきまっている。

中間pathの省略については好みが分かれるところだとは思いますが、自分はonにしていました。

agnoster.zsh-theme

zshに乗り換えてからは agnoster.zsh-theme をカスタマイズして使用していました。 agnoster.zsh-themeはbobthefish以前に開発されたテーマです。

github.com

github.com

f:id:Ryooooooga:20191021213138p:plain
カスタマイズしたagnoster.zsh-theme

変更の内容は

  • 中間path省略の実装
  • 入力位置を次行に
  • 直前の終了ステータスの値を表示
  • 環境変数でセグメントの色や表示/非表示を切り替えられるように
  • RPROMPTに直前のコマンドの実行時間を表示
  • 現在のGitユーザ名を表示

などです。

この1年ほどはこれを使用していましたが、以下のような不満がありました。

  • bobthefishに比べGit statusの情報が荒い (dirty/clean程度の情報しかわからない)
  • zshスクリプトで書かれているのでコードが読みにくい、書きにくい
  • 若干動作がもっさりしているような気がする

今回はzshスクリプトではなくRust、つまりネイティブな言語で実装することでもっさり感と書きやすさを解消したわけです。 ついでにGit statusの解像度をbobthefish程度にまで引き上げました。

他の候補

今回した自作したテーマ以外にも、ネイティブ言語で実装されたテーマは存在しています。

それぞれの特徴と、なぜそれらを使用しなかったかの理由を簡単に述べます。

silver

silver は、同じくRustで書かれたテーマです。bash, zsh, fishに対応しています。

github.com

gitコマンドをプロセスとして呼び出してその出力を利用するのではなく、libgit2を使用することで高速な表示を実現しています。 libgit2関係のコードはこのsilverを参考にさせてもらいました。

このテーマの不満点を以下に挙げます。

  • 入力位置を2行目にすることができない
  • Git statusの解像度がagnoster.zsh-themeと同程度
  • エラーが発生するとすぐにpanicする
  • mac向けのバイナリがGitHub Releaseに用意されていないので cargo install する必要があるが、自分用 dotfiles ではRustのインストールをしないようにしているので扱いが難しい

powerline-go

powerline-go はその名の通りGoで実装されたテーマです。同じくbash, zsh, fishに対応しています。 これは珍しく2行表示に対応したテーマです。

github.com

このテーマの不満点です。

ちなみにこのテーマはlibgit2を使用せず、gitコマンドの出力を利用してブランチなどの情報を表示しています。

自作テーマ

そういうわけでなんとなくテーマを自作することにしました。 開発はRustで行い、silverに倣ってlibgit2を利用しています。

デザインは基本的に (カスタマイズ後の)agnoster.zsh-theme をトレースしています。

また、$XDG_CONFIG_HOME/almel/ 以下 (もしくは $HOME/.config/almel/以下)にあるYAMLファイルを読み込むことでセグメントの表示順や表示の有無、色、アイコンなどを自由にカスタマイズできるようになっています。

表示速度に関しては、ざっくり1.5倍程度速くなりました。(zshのプリコンパイル機能は思ったよりつよい)

f:id:Ryooooooga:20191021224735p:plain
←agnoster.zsh-theme (カスタム) | 自作テーマ →

これから機能の拡張や高速化をしたりドキュメントを整えたりする予定です。 それから、現在はzshにしか対応していないのでbashにも対応させたいです。

インストール

適当にcrates.ioで公開したのでcargoからインストールできます。

cargoを利用したインストール

$ cargo install almel
$ eval "$(almel init zsh)"

zpluginを利用したインストール (現在はmac用のバイナリしか用意していない)

$ zplugin ice from"gh-r" as"program" mv"almel* -> almel"
$ zplugin load Ryooooooga/almel
$ eval "$(almel init zsh)"

セルフホスティングCコンパイラを書いた

セルフホスト(自分自身をビルド)できるCコンパイラnoccを書きました。

github.com

はじめに

去年の夏あたりからCコンパイラを書くのが流行っていたのでやってみました。

例によって@rui314さんの8cc9cc低レイヤを知りたい人のためのCコンパイラ作成入門を参考にしていますが、バックエンドにはLLVMを使用しています。

工夫した点

以下、作る上で工夫した点です。

言語仕様に制限をつける

C言語の全仕様を網羅しようとすると到底完成は不可能なのでサポートする言語仕様に制限をつけまくりました。 制限には例えば以下のようなものがあります。

  • 変数宣言が初期値を取れない。
  • 複数の変数をコンマ区切りで宣言できない。
  • typedef宣言や型のconst修飾などはその語順を固定している。
  • 型解析時は型のconst修飾を無視する。
  • 関数のプロトタイプ宣言はトップレベルでのみ行える。
  • 関数ポインタが使えない。(関数ポインタ自体は実装してあるが関数ポインタ型の変数を宣言できない)
  • 列挙体が使えない。
  • etc

セルフコンパイルに不要そうな機能はあらかた切り捨て、また、そのような機能はできる限り使用せずにコードを書くことで比較的短期間でセルフコンパイルに漕ぎつけることができました。(最初のコミットが2019年4月4日 12:02でセルフコンパイルに成功したのが2019年4月14日 14:30 前後なのでだいたい10日間くらい)

テスト駆動的に開発する

noccの開発はテスト駆動的に行いました。

テストの記述にはLLVMJITコンパイルが非常に役に立ちました。

参考: nocc/test_engine.c at master · Ryooooooga/nocc · GitHub

意味解析器を構文解析器から分離

構文解析器の中に型検査やシンボルの解決などのコードを書くと、コードが肥大化したり見通しが悪くなったりしがちです。

文脈自由言語であれば、構文解析フェーズで(型情報なしの)抽象構文木を生成し、そのあとで木訪問器(Visitor)を使ってその抽象構文木に型情報やシンボルの情報を追加する手法が取られることがあります。

しかし、C言語は文脈依存言語であるため、(typedef宣言をサポートする場合) シンボルの登録や解決を構文解析と同時に行う必要があります。

例えば、a * b;というC言語のコードがあったとして、このコードがどのような意味を持つのかはそれ以前の文脈に依存します。

{
    int a;
    int b;

    a * b; /* ここでは乗算 */
}
{
    typedef int a;

    a * b; /* ここではint*型の変数bの宣言 */
}

そこでnoccではswiftcなどを参考に意味解析器をモジュール化し、構文解析器から呼び出されるコールバック関数として実装することで構文解析と意味解析のコードを分割することにしました。

実際のコードを示しながらどのように分割を行ったか述べます。

識別子式の解析

以下の関数、parse_identifier_expr()は単一の識別子からなる式(a, ctx, printfのような)の構文解析を行う関数です。

ExprNode *parse_identifier_expr(ParserContext *ctx) {
    const Token *t;

    /* トークンを読む */
    t = consume_token(ctx);

    /* トークンが識別子であることをチェックする */
    if (t->kind != token_identifier) {
        fprintf(stderr, "expected identifier, but got %s\n", t->text);
        exit(1);
    }

    /* 意味解析モジュールの呼び出し */
    return sema_identifier_expr(ctx, t);
}

parser.c 298行目 ~ 311行目

見ての通り、実装としてはトークンを1つ読んで識別子かどうかを確かめているだけの単純なものです。

このparse_identifier_expr()から呼び出されている関数、sema_identifier_expr()が意味解析モジュールのコールバック関数です。

これは1つの識別子トークンを受け取り、シンボルの解決や型情報の解決を行ったあとその式を表す抽象構文木のノードを返す関数です。

ExprNode *sema_identifier_expr(ParserContext *ctx, const Token *t) {
    /* 抽象構文木のノードを表す構造体 */
    IdentifierNode *p;

    /* ノードの構築 (省略) */

    /* 現在のスコープからシンボルの宣言を探す */
    p->declaration = scope_stack_find(ctx->env, t->text, true);

    if (p->declaration == NULL) {
        /* シンボルが見つからなかった */
        fprintf(stderr, "undeclared symbol %s\n", t->text);
        exit(1);
    }

    /* (省略) */

    /* 式の型は宣言された変数の型と同じ */
    p->type = p->declaration->type;

    /* 構築したノードを構文解析器に返す */
    return (ExprNode *)p;
}

sema.c 399行目 ~ 434行目

[2019/04/15 追記]

次はより複雑な文の解析を行う場合を見てみます。

複合文の解析

{ ... } によって表現される複合文 (Compound Statement)はレキシカルスコープを形成します。

この文の構文解析は以下の関数、parse_compound_stmt()によって行われます。

StmtNode *parse_compound_stmt(ParserContext *ctx) {
    const Token *open;
    const Token *close;
    Vec *stmts;

    /* 複合文の形成するレキシカルスコープに入る */
    sema_compound_stmt_enter(ctx);

    /* トークンを読む */
    open = consume_token(ctx);

    /* トークンが 「{」 であることを確認する */
    if (open->kind != '{') {
        fprintf(stderr, "expected {, but got %s\n", open->text);
        exit(1);
    }

    /* 内部の文の抽象構文木ノードを格納するvectorを生成する */
    stmts = vec_new();

    /* 「}」 トークンが出現するまで繰り返し文を解析する */
    while (current_token(ctx)->kind != '}') {
        vec_push(stmts, parse_stmt(ctx));
    }

    /* トークンを読む */
    close = consume_token(ctx);

    /* トークンが 「}」 であることを確認する */
    if (close->kind != '}') {
        fprintf(stderr, "expected }, but got %s\n", close->text);
        exit(1);
    }

    /* 抽象構文木ノードを生成し、レキシカルスコープから抜ける */
    return sema_compound_stmt_leave(ctx, open, (StmtNode **)stmts->data,
                                    stmts->size, close);
}

parser.c 835行目 ~ 869行目

上のコードでは、識別子式の例とは異なり意味解析モジュールが2回呼び出されています。

  1. sema_compound_stmt_enter() 関数
  2. sema_compound_stmt_leave() 関数

これらはそれぞれ「{」の出現時、「}」の出現時に呼び出されるコールバック関数であり、複合文が形成するレキシカルスコープの生成と破棄を担当します。

/* 複合文の開始時に呼び出されるコールバック関数 */
void sema_compound_stmt_enter(ParserContext *ctx) {
    /* 新しいスコープをスタックにプッシュする */
    sema_push_scope(ctx);
}

/* 複合文の終了時に呼び出されるコールバック関数 */
StmtNode *sema_compound_stmt_leave(ParserContext *ctx, const Token *open,
                                   StmtNode **stmts, int num_stmts,
                                   const Token *close) {
    /* 抽象構文木のノードを表す構造体 */
    CompoundNode *p;

    /* (省略) */

    /* スタックからスコープを取り除く */
    sema_pop_scope(ctx);

    /* ノードの構築 (省略) */

    /* 構築したノードを構文解析器に返す */
    return (StmtNode *)p;
}

sema.c 972行目 ~ 1005行目

[2019/04/15 追記ここまで]

このように構文解析と意味解析を分離する試みは結果的に成功し、構文解析器 (parser.c) と 意味解析器 (sema.c) 双方の見通しの良さを保つことができました。

まとめ

コード生成はLLVMにおんぶにだっこであんまりちゃんと書けた気がしません。

気が向いたらx86_64のコードを吐けるようにしてみようと思います。

JITコンパイル時の関数呼び出しの扱い方

x86_64での関数呼び出し

x86_64(以下x64)ではcall命令の呼び出し関数の指定を相対アドレスで行うため、JITコンパイルをする際はそのアドレスの取り扱いに苦労します。

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main(void) {
    const char code[] = {
        /*
         * int f(void) {
         *     return add(2, 3);
         * }
         */
        0xbe, 0x03, 0x00, 0x00, 0x00, // mov esi, 3
        0xbf, 0x02, 0x00, 0x00, 0x00, // mov edi, 2
        0xe8, 0x01, 0x00, 0x00, 0x00, // call +1 # (相対アドレスでaddを指す)
        0xc3,                         // ret
        
        /*
         * int add(int x, int y) {
         *     return x + y;
         * }
         */
        0x89, 0xf8, // mov eax, edi
        0x01, 0xf0, // add eax, esi
        0xc3,       // ret
    };
    
    const size_t size = sizeof(code);
    
    // 実行可能なメモリ領域を確保する
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    if (ptr == MAP_FAILED) {
        return 1;
    }
    
    memcpy(ptr, code, size);
    
    // 機械語を呼び出す
    int (*f)(void) = ptr;
    printf("%d\n", f());  // 5
    
    // 確保した領域を解放する
    munmap(ptr, size);

    return 0;
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

上の例では、相対アドレス+0x00000001を指定して、その直後に定義される関数addを呼び出しています。

0xe8, 0x01, 0x00, 0x00, 0x00, // call +1 # (相対アドレスでaddを指す)

呼び出す関数がこのように同一のコードセグメントに含まれる場合はこのように指定すればいいのですが、 標準ライブラリの関数やコンパイラ側で定義した関数の呼び出しを行う場合は呼び出しアドレス指定の取り扱いに困ります。

// C言語側で定義された関数
int sub(int a, int b) {
    return a - b;
}

//------------------------

0xe8, 0x??, 0x??, 0x??, 0x??, // call sub (subの相対オフセットがわからない)

レジスタを経由して絶対アドレス指定で関数を呼び出す

このような場合は、レジスタ経由で呼び出すと絶対アドレス指定ができるため、取り扱いが簡単になります。

mov rax, <subの絶対アドレス>
call rax

これをコードにすると次のようになります。

int sub(int a, int b) {
    return a - b;
}

//-----------------------

char code[] = {
    /*
        * int f(void) {
        *     return sub(2, 3);
        * }
        */
    0xbe, 0x03, 0x00, 0x00, 0x00,       // mov esi, 3
    0xbf, 0x02, 0x00, 0x00, 0x00,       // mov edi, 2
    0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, // mov rax, sub
                0x00, 0x00, 0x00, 0x00, //
    0xff, 0xd0,                         // call rax
    0xc3,                               // ret
};

// subの絶対アドレスを指定する
code[0x0c] = (uintptr_t)sub >>  0;
code[0x0d] = (uintptr_t)sub >>  8;
code[0x0e] = (uintptr_t)sub >> 16;
code[0x0f] = (uintptr_t)sub >> 24;
code[0x10] = (uintptr_t)sub >> 32;
code[0x11] = (uintptr_t)sub >> 40;
code[0x12] = (uintptr_t)sub >> 48;
code[0x13] = (uintptr_t)sub >> 56;

全コード

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>

int sub(int a, int b) {
    return a - b;
}

int main(void) {
    char code[] = {
        /*
         * int f(void) {
         *     return sub(2, 3);
         * }
         */
        0xbe, 0x03, 0x00, 0x00, 0x00,       // mov esi, 3
        0xbf, 0x02, 0x00, 0x00, 0x00,       // mov edi, 2
        0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, // mov rax, sub
                    0x00, 0x00, 0x00, 0x00, //
        0xff, 0xd0,                         // call rax
        0xc3,                               // ret
    };

    code[0x0c] = (uintptr_t)sub >>  0;
    code[0x0d] = (uintptr_t)sub >>  8;
    code[0x0e] = (uintptr_t)sub >> 16;
    code[0x0f] = (uintptr_t)sub >> 24;
    code[0x10] = (uintptr_t)sub >> 32;
    code[0x11] = (uintptr_t)sub >> 40;
    code[0x12] = (uintptr_t)sub >> 48;
    code[0x13] = (uintptr_t)sub >> 56;

    const size_t size = sizeof(code);
    
    // 実行可能なメモリ領域を確保する
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    if (ptr == MAP_FAILED) {
        return 1;
    }
    
    memcpy(ptr, code, size);
    
    // 機械語を呼び出す
    int (*f)(void) = ptr;
    printf("%d\n", f());  // -1
    
    // 確保した領域を解放する
    munmap(ptr, size);

    return 0;
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

structured bindings(構造化束縛)を自作クラスで行えるようにする

はじめに

structured bindings (構造化束縛) をstd::pair<>std::tuple<>以外の自作のクラスに対して適用するための方法をコード例込みで解説している日本語の記事が見つからなかったのでメモを兼ねて残しておきます。

結論

  1. std::tuple_size<T>を特殊化する

  2. std::tuple_element<N, T>を特殊化する

  3. get<N>(T)/T::get<N>()を定義する

structured bindings (構造化束縛)

Structured binding declaration (since C++17) - cppreference.com

構造化束縛 - cpprefjp C++日本語リファレンス

structured bindings は C++17 に追加された有用な機能で、関数の返り値からの多値の受け取りなどを簡便に行える言語機能です。

* 関数から複数の値を受け取る例

// 複数の値を返す関数
std::tuple<int, std::string, double> f() {
    return std::make_tuple(42, "foo", 3.14);
}

int main() {
    // 関数からの返り値を受ける
    auto [n, s, d] = f();
    
    assert(n == 42);
    assert(s == "foo");
    assert(d == 3.14);
}

上の例を structured bindings を使わずに書くと例えば次のようになります。

* structured bindings を使わずに関数から複数の値を受け取る例

int main() {
    int n;
    std::string s;
    double d;

    std::tie(n, s, d) = f();
    
    assert(n == 42);
    assert(s == "foo");
    assert(d == 3.14);
}

structured bindings は、std::tuple<>以外にもいくつかの型についてその恩恵を受けることが出来ます。

* std::tuple<>以外の多値型を受け取る例

{
    // std::pair<>
    auto [a, b] = std::make_pair("bar", 100);
}
    
{
    // 生配列
    int raw_array[3] = {1, 2, 3};
    
    auto [a, b, c] = raw_array;
}
    
{
    // std::array<>
    std::array<int, 3> array = {1, 2, 3};
    
    auto [a, b, c] = array;
}

{
    // 構造体
    // 非staticなメンバ変数はすべてpublicである必要がある
    struct S {
        std::string a;
        int b;
    };
    
    auto [a, b] = S { "bar", 10 };
}

自作クラスに structured bindings を適用する

上記に当てはまらないようなクラスについても structured bindings を適用することが出来ます。

例えば次のような privateな 非staticメンバ変数を持つクラスColorを考えます。

* 自作クラス

class Color {
    std::uint32_t c;

public:
    Color(std::uint32_t c) : c(c) {}

    std::uint8_t red() const { return c >> 16; }
    std::uint8_t green() const { return c >> 8; }
    std::uint8_t blue() const { return c >> 0; }
};

(Colorは内部に色の16進表現を保存し、メンバ関数red(), green(), blue() でRGBそれぞれの色要素を取り出せるようなクラスを想定しています)

これをauto [r, g, b] = ...のように受け取りたいのですが、そのままでは当然エラーになってしまいます。

int main() {
    Color orange = 0xff8000;

    auto [r, g, b] = orange; // エラー: cannot decompose non-public member 'Color::c' of 'Color'
}

一度std::tuple<>に変換する関数を用意してauto [r, g, b] = orange.as_tuple();のように書いてもいいのですが直截的でないので別の方法をとります。

1. std::tuple_size<>を特殊化する

まずはじめに、structured bindings で受け取る変数の個数をコンパイラに示します。 コンパイラstd::tuple_size<>::valueの値でこれを確認するため、std::tuple_size<Color>を特殊化します。

* std::tuple_size<>の特殊化

namespace std {
    template <>
    struct tuple_size<Color> : integral_constant<size_t, 3> {}; // Color は 3要素である
}

2. std::tuple_element<>を特殊化する

次に要素の型をコンパイラに示します。 これにはstd::tuple_element<>::typeが使われるため、std::tuple_element<N, Color>を特殊化します。

* std::tuple_element<>の特殊化

namespace std {
    template <size_t N>
    struct tuple_element<N, Color> {
        using type = uint8_t; // 要素の型はすべて std::uint8_t
    };
}

3-a. フリー関数get<>()を定義する

最後に実際に要素の値を取り出します。 これには関数get<N>(Color)が用いられるのでそれを定義します。

使用される関数はADLによって探索されるので目的のクラスと同じ名前空間に定義すれば良いでしょう。

* get<>()の定義

template <std::size_t N>
uint8_t get(const Color& c) {
    if constexpr (N == 0)
        return c.red();
    else if constexpr (N == 1)
        return c.green();
    else
        return c.blue();
}

以上で自作クラスColorに structured bindings を適用することが可能になります。

* Colorに対する structured bindings

int main() {
    Color orange = 0xff8000;
    
    auto [r, g, b] = orange;
    
    assert(r == 255);
    assert(g == 128);
    assert(b ==   0);
}

3-b. メンバ関数get<>()を定義する

フリー関数get<>()の代わりにメンバ関数get<>()を使用することも可能です。

* メンバ関数Color::get<>()の定義

class Color {
    ...

    template <std::size_t N>
    std::uint8_t get() const {
        if constexpr (N == 0)
            return red();
        else if constexpr (N == 1)
            return green();
        else
            return blue();
    }
};

フリー関数get<N>(Color)と、メンバ関数Color::get<N>()が同時に定義されていた場合、メンバ関数Color::get<N>()が優先的に使用されます。

まとめ

structured bindings はGCC/ClangのみでなくMSVCでも利用することの出来る優れた機能です。

C++は複数の値を関数から返すことのできない言語だ」などと揶揄されることもしばしばありましたが今やその限りではありません。

* 全コード

#include <cassert>
#include <tuple>
#include <iostream>

class Color {
    std::uint32_t c;

public:
    Color(std::uint32_t c) : c(c) {}

    std::uint8_t red() const { return c >> 16; }
    std::uint8_t green() const { return c >> 8; }
    std::uint8_t blue() const { return c >> 0; }
    
    template <std::size_t N>
    std::uint8_t get() const {
        std::cout << "Color::get<" << N << ">()" << std::endl;
    
        if constexpr (N == 0)
            return red();
        else if constexpr (N == 1)
            return green();
        else
            return blue();
    }
};

template <std::size_t N>
uint8_t get(const Color& c) {
    std::cout << "get<" << N << ">(Color)" << std::endl;
    
    if constexpr (N == 0)
        return c.red();
    else if constexpr (N == 1)
        return c.green();
    else
        return c.blue();
}

namespace std {
    template <>
    struct tuple_size<Color> : integral_constant<size_t, 3> {}; // Color は 3要素である
    
    template <size_t N>
    struct tuple_element<N, Color> {
        using type = uint8_t; // 要素の型はすべて std::uint8_t
    };
}

int main() {
    Color orange = 0xff8000;
    
    // こう受け取りたい
    auto [r, g, b] = orange;
    
    assert(r == 255);
    assert(g == 128);
    assert(b ==   0);
}

MSVCでempty base optimizationの効かないパターンと対策

問題

2つ以上の空クラスを継承するようなクラスについて、MSVCで empty base optimization (EBO) が期待した通りに働かず、余分な領域が消費される。

struct Empty1 {};
struct Empty2 {};

// 1つの空クラスを基底に持つ
struct Derived1 : Empty1 {
    int i;
};

// 2つの空クラスを基底に持つ
struct Derived2 : Empty1, Empty2 {
    int i;
};

static_assert(sizeof(Empty1) == 1);
static_assert(sizeof(Empty2) == 1);

static_assert(sizeof(Derived1) == sizeof(int));
static_assert(sizeof(Derived2) == sizeof(int)); // MSVCにてエラー

対策

__declspec(empty_bases)拡張属性をEBOを期待するクラスの宣言に付与する。

struct __declspec(empty_bases) Derived2 : Empty1, Empty2 {
    int i;
};

static_assert(sizeof(Derived2) == sizeof(int));

参考: Optimizing the Layout of Empty Base Classes in VS2015 Update 2 | Visual C++ Team Blog

empty base optimization

C++では空クラス(メンバ変数を持たず、仮想メンバ関数を持たないクラス)のサイズは必ず 1以上の大きさを持ちます。 (現行のほとんどの環境で空クラスのサイズは1になります)

struct Empty {};

static_assert(sizeof(Empty) > 0); // 多くの環境で sizeof(Empty) == 1

EBOはその様な空クラスを継承する際のメモリレイアウトに関する最適化の一種で、 継承先のクラスに余分な領域を確保しないようにする効果があります。

struct Empty {}; // 空クラス
struct NotEmpty { char c; }; // 空でないクラス

// どちらもサイズは1byte
static_assert(sizeof(Empty)    == 1);
static_assert(sizeof(NotEmpty) == 1);

// 基底クラスが空クラスなのでEBOが効く
struct A : Empty { int i; };

static_assert(sizeof(A) == sizeof(int));

// 基底クラスが空でないので
// メンバ変数と基底クラスの領域 (+ アライメントのためのパディング)分のサイズを必要とする
struct B : NotEmpty { int i; };

static_assert(sizeof(B) > sizeof(int));

例えばsizeof(int) == 4, alignof(int) == 4であるようなある環境ではsizeof(A) == 4, sizeof(B) == 8となりました。

MSVCでの動作

MSVCではただ1つの空クラスを継承するようなクラスに関して他のコンパイラ同様EBOが働きますが、 2つ以上の空クラスを継承する場合 (デフォルトでは) EBOが働きません

上に挙げた参考サイトの記述によると、

The Visual C++ compiler has historically had limited support for EBCO; however, in Visual Studio 2015 Update 2, we have added a new __declspec(empty_bases) attribute for class types that takes full advantage of this optimization.

VS2015 Update 2 以降では クラスの宣言に__declspec(empty_bases)を付与することで複数の空クラスを継承する場合にもEBOが有効になる様です。

struct Empty1 {};
struct Empty2 {};

// 定義に __declspec(empty_bases) を付ける
struct __declspec(empty_bases) Derived1 : Empty1, Empty2 { int i; };

static_assert(sizeof(Derived1) == sizeof(int)); // EBOが効くようになった

// または前方宣言に __declspec(empty_bases) を付ける
struct __declspec(empty_bases) Derived2;

struct Derived2 : Empty1, Empty2 { int i; };

static_assert(sizeof(Derived2) == sizeof(int)); // EBOが効くようになった

MSVCに於いてこのような場合のEBOが依然としてデフォルトで有効にならないのはABI互換性を保つためのようです。

他のコンパイラでもビルドを出来るようにするにはマクロを使った工夫が必要になるでしょう。

#if defined(_MSC_VER) && _MSC_FULL_VER >= 190023918
#  define EMPTY_BASES __declspec(empty_bases) // VS2015 Update 2 以降
#else
#  define EMPTY_BASES
#endif

struct EMPTY_BASES Derived : Empty1, Empty2 { int i; };

static_assert(sizeof(Derived) == sizeof(int));

見た目がダサい

どのような場合に問題になるか

クラス内部のメモリレイアウトに依存したプログラムを書くような場合、EBOが効かないことが問題となります。

理想的にはクラスのメモリレイアウトに極力依存しないことが理想ではありますが、実際問題としてC/C++では特定のメモリレイアウトを期待するようなプログラムを書く必要に迫られる場面も少なからず存在します。

そのような時、プログラムが予想と違う動作をした場合には実際にメモリ上にどの様にクラスが展開されているのかを注意深く確認しましょう。