SNSへはこちら

【速報?】GCC10.1がリリース!constexpr周りで遊んでみる

ついに来ましたね。そう、C++20 を正式にサポートした GCC 10 がリリースされました!

(僕の基準だと)ようやく近代の言語規格となった C++20 ですが、ついに概念から現実になりました。本記事は取り敢えず新機能で遊んでみるという内容でやっていきます。

ご注意

  • あくまでも遊んで見るだけです。詳細な言語規格の把握はしていませんのでご了承を。
    • なんなら GCC 10 のリリースノートでさへ読んでいない(←
  • 筆者の C++ スキルを示しておきます。今後より詳しい人の解説が期待されます(丸投げ)。
    • 組込みで C++17 を使う人間。マイクロマウスでテンプレートとか使いまくっている
    • STL や algorithm ヘッダは結構使っている。
    • SFINAE は吐き気を催すほど嫌い。スキルが低くて上手く書けません。
    • 組込み用途で使う機能しか興味ない。new とかヒープ食べるものはやりたくないです。
    • なので知識が偏ってます。
  • コンパイル時のオプションは -Os -std=c++20 -Wall

動作環境は Ubuntu 20.04 です。Mac はとにかくネイティブ環境でのビルドが通らないので放り投げました。今後要研究。

C++20だ

ついに来ました。C++ と言ったら、低レイヤをいじることができるくせに、強力なコンパイル時計算が出来るいい感じの言語という認識でいます。割とモダンなのに、ひねったことをするまでもなく組込みプログラミングに使えるので大好きです。
と同時に、C 言語とは違って、デフォルトでアルゴリズムやデータ構造が提供されているのでいちいち車輪の再発明を繰り返さなくてもいいし、だからこそバグが生まれにくいと考えます。色々と落とし穴はあるんだけど。

では行ってみよう

GCC 9 系の -std=c++2a では出来なかった機能を試してみました。さあ!

その前に、Ubuntu でビルドした際のコマンドを雑に示しておきますね。

$ contrib/download_prerequisites
$ ../configure --prefix=/usr/local/gcc10 --enable-languages=c,c++ --disable-werror --disable-bootstrap --disable-multilib
$ make -j8
$ sudo make install

consteval

constexpr というのがありました。このキーワードを付けると「コンパイル時に計算できるよ!」となります。でもまあコンパイル時計算をしなければならないという強い成約を与えるものではないので、実行時まで計算を先延ばしにする事がありました。

僕の組込み経験だとそういうことは無かったと記憶しているのですが、でも言語規格としてコンパイル時計算を強制するものではないとされているので、若干不安ですよね。
C++20 で登場した consteval では、強制が働くので安心してコンパイル時計算ライフを楽しめるわけです。

constexprconsteval の違いがわかるコードを書くことが出来ませんでしたが、取り敢えずコードを書きました。こんなんでどうでしょう。

#include <array>
#include <numeric>

template <auto N>
consteval int sum(const std::array<int, N>& arr) {
    return std::accumulate(std::begin(arr), std::end(arr), 0);
}

int main() {
    constexpr std::array<int, 5> arr = {1, 2, 3, 4, 5};
    return sum(arr);
}

出来上がったバイナリを逆アセンブルするとこんな感じ。確かにコンパイル時計算されてますね。

セクション .text の逆アセンブル:

0000000000401020 <main>:
  401020:   b8 0f 00 00 00          mov    $0xf,%eax
  401025:   c3                      retq   
  401026:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40102d:   00 00 00 

new/delete

なんと、new/delete も constexpr 化される時代です。驚きですよ。

#include <numeric>

struct MyClass {
    int val;
    MyClass operator+(const MyClass c) const {
        return {c.val + this->val};
    }
};

constexpr auto f(int x, int n) {
    auto* p = new MyClass[n] {{}};

    for(int i = 0; i < n; i++) {
        auto num = x + i;
        p[i].val = num * num;
    }
    return std::accumulate(p, p + n, (MyClass){0}).val;
}

int main() {
    return f(5, 3);
}

衝撃の逆アセンブル結果をご覧あれ。

セクション .text の逆アセンブル:

0000000000401020 <main>:
  401020:   b8 6e 00 00 00          mov    $0x6e,%eax
  401025:   c3                      retq   
  401026:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40102d:   00 00 00

戻り値が... 0x6e == 110 だと計算されていますね...すごい...

コンセプト

SFINAE の役割の大部分を置き換えると言われているコンセプトです。確か以前は C++17 に入るかもとか言われていて、結局お流れになりましたね。
これまでは -fconcepts というコンパイラオプションを付ける必要がありましたが、今は関係ありません。C++20 においては完全に合法です。

#include <numeric>

template <class T>
concept Addable = requires(T&& a, T&& b) {
    a + b;
};

template <Addable T>
T sum(T&& a, T&& b) {
    return a + b;
}

struct Age {
    unsigned int val;
    Age operator+(const Age a) const {
        return {a.val + this->val};
    }
};

int main() {
    return sum(Age{10}, Age{52}).val;
}

しかも逆アセンブル結果もすごくいい感じですね。

セクション .text の逆アセンブル:

0000000000401020 <main>:
  401020:   b8 3e 00 00 00          mov    $0x3e,%eax
  401025:   c3                      retq   
  401026:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40102d:   00 00 00 

Rangeライブラリ

これはよく知らなかったのですが、調べたらどうも便利なようで、見つけました。
使ってみた感想ですが、C# の LINQ みたいだな と感じました。

#include <iostream>
#include <ranges>
#include <array>

int main() {
    std::array<int, 10> arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    using namespace std::ranges::views;
    for( auto item : arr | reverse ) {
        std::cout << item << std::endl;
    }
}
$ ./a.out
10
9
8
7
6
5
4
3
2
1

へぇ〜〜〜
では、フィルターをかけてみましょう。

#include <iostream>
#include <ranges>
#include <array>

int main() {
    std::array<int, 10> arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    using namespace std::ranges::views;
    for( auto item : arr
        | reverse
        | filter([](auto &x){return (x & 1) == 0;})
    ) {
        std::cout << item << std::endl;
    }
}
$ ./a.out 
10
8
6
4
2

これは楽しい。まさに LINQ とか、シェル芸的な感覚ですね。超便利です。

実装されていないと確認したもの

実際に動かしてみたのは以上です。以下はやってみたもののエラー等で動かなかったものです。

  • scoped-enumのローカルスコープでのusing
  • 一部STLのconstexpr化
  • import <iostream>

今後の実装に期待します!