プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

jbangがJavaの初学者にめっちゃ便利な件について

 jbangという、Java をスクリプト言語のように実行できるツールを知りました。

 これ、プログラミング初心者が Java を学び始めるのにとても有用だと思います。

  • JDKのインストールが不要
    • 準備が大体コピペだけですむ
  • コンパイルせずに、.javaファイルを直接実行可能
  • Gradle (ANT, Maven) が無くても外部依存関係を解決できる

 とても良い感じです。

 初心者にいきなりコンパイル→実行をさせたり、もしくはIDEを使わせるというのは、結構ハードルが高いと思います。 たださえ、意味の分からないことをやらされているのに、いきなり意味不明なコンパイルを挟まないと実行できないとか、大量のボタンと画面が殴りかかってくる恐怖のIDEとか、乗り越えるのも嫌になるでしょう。

 我々からすれば「その程度のこと」ですが、割と「その程度のこと」で躓く物です。

 JShellは初心者にとても良いと思いますが、 外部依存関係は解決できません。 jbangはその点が非常に優れていると思います。

 

何も準備せずに Hello World を動かしてみた

 Javaの入っていない Windows 10 環境を用意しました。java も javac も入っていません。

f:id:nodamushi:20201012214910p:plain:w320

 作業ディレクトリとして、デスクトップに jbang を配置。

f:id:nodamushi:20201012220113p:plain:w320

 次に PowerShellを起動します。

 なお、PowerShellを開く/触るという行為は、初心者には拒絶されるレベルで、ハードルが高いです。みなさん、頑張って下さい。

 「Shift + 右クリック」をすると 「PowerShell ウィンドウをここで開く」が出るのでこれをクリックします。

  f:id:nodamushi:20201012220243p:plain:w320

 

 PowerShellが開いたら、以下をおもむろにコピーして、PowerShellに貼り付けます。

 なお、Scoopがすでに入ってる場合は、3行目からでOK。gitがすでに入っている場合は、3行目はスキップ可能。

Set-ExecutionPolicy RemoteSigned -scope CurrentUser
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
scoop install git
scoop bucket add jbangdev https://github.com/jbangdev/scoop-bucket
scoop install jbang

 PowerShell はGUIじゃないので、かなりハードルが高いですが、コピペだけでインストールがすむので、まだ何とかなるのではないでしょうか。

 

 次に、メモ帳(もしくはさくらエディタ)を開いてもらいます。初心者でも、何故か、さくらエディタを使ってる確率が高い気がします。

 そして、次の内容をエディタで入力してもらい、「Hello.java」として保存します。

 なお、jbang init Hello.java を使えばメモ帳不要ですが、個人的には手を動かしてみる体験は重要だと思うので、ここはメモ帳を起動した方が良いと思います。

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello World");
  }
}

1行目の「///usr/bin/env jbang "$0" "$@" ; exit $?」は Windowsでは不要  

f:id:nodamushi:20201012221108p:plain:w320

 

 富士山の頂上から、麓まで飛び降りるレベルの崖っぷちとして、「Hello.java.txt」として保存してしまうことがあげられます(上記画像)。何とか上手く突破させてあげてください。

 後はおもむろに jbang Hello.java と PowerShell で実行するだけで、勝手に JDK のダウロードをしてから、実行してくれます。(環境変数 JAVA_HOME がある場合は、そちらを使います)

f:id:nodamushi:20201012221535p:plain

 無事、Java を何も入れていない環境で、あっさりハロワが完了しました!

 なお、jbangがコンパイルした結果や、ダウンロードしたJDKは、ユーザーホームディレクトリの .jbang/cache にあります。  

 次に、jbang のサンプルにもあるように、jfiglet ライブラリを使ったプログラムを実行してみましょう。

//DEPS com.github.lalyos:jfiglet:0.0.8

import com.github.lalyos.jfiglet.FigletFont;

public class Hello {
  public static void main(String[] args) throws Exception{
    System.out.println(FigletFont.convertOneLine("Hello World"));
  }
}

  f:id:nodamushi:20201012225254p:plain

 //DEPS com.github.lalyos:jfiglet:0.0.8 を読み取り、勝手に依存を解決して実行してくれます。

 外部の依存するライブラリを使う為にビルドスクリプトや、ダウンロードを手動ですること無しに簡単にできました。

 今後Gradleなどを教える際の、敷居が少し下がるのでは無いでしょうか。

 

まとめ

 jbangを使えばJDKをインストールしなくて良いというのは、非常に初学者に優しいと思います。

 JShellとならんで、初学者の助けになるツールになるポテンシャルを感じました。

 jbangからJShellが起動できるともっと良いかな。※追記

追記: jbangからJShellを操作する

 どうやら、 jshファイルを --interactive を付けて実行すると、JShellを操作できるようです。

f:id:nodamushi:20201013003207p:plain

Visual StudioのEmacs Emulationで選択範囲を一気に消せるぞ!

 Visual Studio(Codeじゃないよ)には Emacs Emulation 2 という拡張機能がありますが、ちょっと微妙です

 特に私を苛つかせるのが、コレ

 ①範囲を選択する f:id:nodamushi:20201006024259p:plain

 

 ②Delete(Ctrl + Dなども) f:id:nodamushi:20201006024416p:plain

 アバババババババババ ヽ(゜ロ。#)ノ

 範囲選択→削除なんて日常茶飯事にやる行為が動かないのです ストレスマッハです

 というわけで、 Visual Studioはよほどのことが無い限り使えないのですが、2020年になってから、なんと直してくれてる人がいたっ!!

github.com

https://github.com/zbrad/EmacsKeys/issues/16

 

 私が試した範囲では、きちんと動くので、かなりストレスが減ります

 ③TTimoさんのパッチ版 f:id:nodamushi:20201006025157p:plain

 TTimoさんがプルリク等を出してる様子はないので、試してみたい方は自分でビルドしてインストールしてみると良いと思います。

  1. (Emacs Emulationをインストールしてる人は、拡張機能の管理から削除)
  2. GitHub - TTimo/EmacsKeys: Emacs key bindings for Visual Studio からコードをダウンロード/クローン
  3. Visual Studioでプロジェクトを開く (EmacsEmulation.slnをダブルクリックで開くよ)
  4. Releaseモードでビルド
  5. Visual Studioを閉じる
  6. 「bin/Release/EmacsEmulation.vsix」が出来ているので、ダブルクリックしてインストール
  7. Visual Studioを起動後、ツール > オプション > 環境 > キーボードの 「次の追加キーボードマップスキームを適用」 で「Emacs」を選ぶ
  8. 必要な設定をする(Ctrl+Hとか)
  9. 選択範囲の削除を試してみる

 

 動けばだいぶ幸せになれますね

Emacsで Mozilla rr を使えるようにしてみた

 Windows野郎で未だにWSL1の情弱である私も、昨日の記事で遂に Mozilla rr が使えるようになりました。

nodamushi.hatenablog.com

 しかし、Emacsで使いにくい。Emacs用のパッケージも特に無さそう?というわけで、設定追加したので晒してみる。 -exec-reverse-continue見たいなコマンドはないっぽいけど、取りあえず、何か何とか動いてます。

 基本的には gdb を呼び出してるだけです。

  • M-x → rr : rr replayを起動
  • M-x → rr-record: rr recordを起動
  • F8: continue
  • Ctrl + F8: reverse-continue
  • F9: step
  • Ctrl + F9: reverse-step
  • F10: next
  • Ctrl + F10: reverse-next
  • F11: finish
  • Ctrl + F11: reverse-finish
(use-package gdb-mi
  :init
  (defcustom gud-rr-command-name "rr replay -i=mi"
    "Default command to execute an executable under the Mozilla rr debugger."
    :type 'string
    :group 'gdb)
  (defcustom gud-rr-record-command-name "rr record"
    "Default command to execute an executable under the Mozilla rr recode."
    :type 'string
    :group 'gdb)

  (defun rr (command-line)
    "rr replay"
    (interactive (list (gud-query-cmdline 'rr "")))
    (gdb command-line)
    (gud-def gud-reverse-next "reverse-next %p"
             "\C-r"
             "Step one line (skip functions).")
    (gud-def gud-reverse-step "reverse-step %p"
             "\C-e"
             "Step one line (skip functions).")
    (gud-def gud-reverse-continue "reverse-continue"
             "\C-w"
             "Step one line (skip functions).")
    )
  
  (defun rr-record (command-line)
    "rr record"
    (interactive (list (gud-query-cmdline 'rr-record)))
    (compile command-line))

  :config

  ;; global-display-line-numbersを有効にしてるので、変数バッファなどで無効化
  (defun my-disable-display-line-numbers (orig-func &rest args)
    (let ((r (apply orig-func args)))
      (remove-hook 'pre-command-hook #'display-line-numbers-update-width t)
      (setq display-line-numbers nil)
      r))
  (advice-add 'gud-common-init :around #'my-disable-display-line-numbers)
  (advice-add 'gdb-get-buffer-create :around #'my-disable-display-line-numbers)

  (global-set-key (kbd "<f8>") 'gud-cont)
  (global-set-key (kbd "C-<f8>") 'gud-reverse-continue)
  (global-set-key (kbd "<f9>") 'gud-step)
  (global-set-key (kbd "C-<f9>") 'gud-reverse-step)
  (global-set-key (kbd "<f11>") 'gud-finish)
  (global-set-key (kbd "C-<f11>") 'gud-reverse-finish))

f:id:nodamushi:20200702223541p:plain

Hyper-VにUbuntu20.04を入れてから、 Mozilla rr を動かすまで

 Mozilla rr という、実行のイベントを保存して、プロセスの実行を再現するデバッガを今更最近知りました。 ステップ実行を逆方向に行えるので、とても便利ですね。

 仕組み的にはWSL2なら使えそうな気がしますが、WSL1の私は使えません。(ていうか、Windows 2004は一体いつなんだ…。)

 仕方がないので、Hyper-V上のUbuntuで動かそうとしましたが、はい、動きません。チキショウ。

 調べてみると、Wiki曰く 「Hyper-V does not seem to support PMU virtualization.」とのことで、 Hyper-V上では動かないのだ。

 マジかよー、と一度は諦めたのですが、もう一度よく調べたら何かできたのでここにメモしておきます。

 ちなみに、初めてUbuntu 20.04 も入れたので、そこからだぜ。

Ubuntu 20.04 のインストール

 実は初めてのUbuntu 20.04。サクッと Hyper-V クイック作成から突っ込みます。

f:id:nodamushi:20200702004506p:plain:w320

 プロセッサやメモリや統合サービス等の設定を適当にして、起動。

f:id:nodamushi:20200702004721p:plain:w320

 サクッとインストールして、起動したのがこちら。

 ………リージョンを日本語にしていますが、全然翻訳されてない。

f:id:nodamushi:20200702005210p:plain:w320

 翻訳はされていませんが、ディレクトリは「デスクトップ」とかいう日本語になってるので、「Desktop」に変更。

 最初からEnglishを選んだ方が楽だと思う。

LANG=C xdg-user-dirs-gtk-update

f:id:nodamushi:20200702005521p:plain:w320

拡張セッション

 このままじゃクリップボードの共有が出来なくて辛タンなので、Microsoft linux-vm-toolsでxrdpを入れて、拡張セッションを使えるようにします。

sudo apt install git
mkdir ws
cd ws
git clone https://github.com/microsoft/linux-vm-tools.git
cd linux-vm-tools/ubuntu

 20.04が…………ない?あれー?

f:id:nodamushi:20200702010041p:plain

 2020年7月現在だと、Ubuntu 20.04に対応していませんでした。

 20.04対応のプリリクを出して下さっている方が居るので、そっちで試してみます。

(※未来の方で20.04があるなら、それを利用して下さい。)

f:id:nodamushi:20200702010412p:plain:w320

git clone -b ubuntu20-04 https://github.com/Hinara/linux-vm-tools.git
cd linux-vm-tools/ubuntu/20.04
chmod +x install.sh

 で、 install.shを実行してreboot

sudo ./install.sh
sudo reboot

 rebootしたらもう一回 install.shを実行し、シャットダウンします。

cd ~/ws/linux-vm-tools/ubuntu/20.04
sudo ./install.sh
sudo shutdown now

 Windows上で管理者権限のPowerShellを起動し、以下を実行。 Ubuntu 20.04の所は自分の仮想マシン名に合わせてください。

Set-VM -VMName "Ubuntu 20.04" -EnhancedSessionTransportType HvSocket

 これで、一応拡張セッションは使えるようになったんですが、何かやたらと反応するかしないかが不安定です。4回に一回ぐらいしか、拡張セッションが有効にならない。よくわからん。なーんか、「ゲスト サービス」を有効にしてるとダメな感じがする…(不明)

Mozilla rr を使えるようにする

 Windows Server 2019 および Windows 10 バージョン1809以降のバージョンのみ可能です。IntelのCPU以外は知らない。Ryzen欲しい。

 先ずは Windows上で PowerShellを管理者権限で起動して、以下を実行。Ubuntu 20.04の部分は仮想マシン名に合わせてください。

 なお、たぶん、 PMUだけで十分だとは思われる。

Set-VMProcessor "Ubuntu 20.04" -Perfmon @("pmu", "lbr", "pebs")

t.co

 そして、Ubuntu20.04を起動して、rrをインストール

sudo apt install rr

 で、以下の様な適当なCコードを作成して、デバッグビルドしてrrで記録を取ってみる。

 初回は必ず /proc/sys/kernel/perf_event_paranoid が3だと怒られるので、1に変更しておきます。

 図のメッセージのように、 /etc/sysctl.conf にkernel.perf_event_paranoid = 1 を書くかどうかはご自由に。(私は面倒くさいので設定しましたが、どういう影響が出るのか良く分からないので…。まぁ、どうせ仮想マシンだし)

f:id:nodamushi:20200702023835p:plain

gcc -g3 foo.c
echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid
rr record ./a.out
rr replay

 これで上の図のようにちゃんとHyper-V上のUbuntu20.04でMozilla rrが動きました!やったね!

JavaのOptionalは現状失敗作

 チマチマとは書いていたんですが、1年ぶりぐらいに真面目にJava書いています。

 前からずっと思っていて、実際何度か口にしたことがあるんですが、やっぱ言いたい。

 JavaのOptionalは失敗作

 

 

JavaのOptionalの何が悪いの?

 言語機能じゃないから。完全にここに集約されると私は思っています。

 Java8で登場したOptionalですが、この時にコンパイラを拡張しなかったのがそもそもの問題だと思っています。

 

AutoBoxing

 例えばKotlinのnullableのような機能の導入、というのは慎重になるべきでしょうし、すぐできるとは思えません。

 しかし、Optionalへのオートボクシングぐらいは実現できたのではないでしょうか?int→Integerというオートボクシングは既に存在しているわけで、最初から導入すべきだったと思います。

public OptionalInt  intMethod(){
     if(何か条件) return 1;  // -> OptionalInt.of(1)に
     if(何か条件) return OptionalInt.of(1);  // そのまま
     return null;// -> OptionalInt.empty()に
}

public Optional<Object> objMethod(){
    Object obj = null;
    if(何か条件)   return new Object(); //-> Optional.of(new Object())に
    if(何か条件)   return obj; // -> Optional.ofNullable(obj)に
    return null; // -> Optional.empty()に
}

 

 実に鬱陶しいofやらemptyやらofNullableが省略できる上、Optionalの癖にnullが返せるという謎の状況も生まれにくいでしょう。(※上の例だと、OptionalIntをそのまま返すとしてるけど、ここでnull挿入できる)

 むろん、色々な議論があったのでしょうが、導入が許されるのは、最初だけなのです。オートボクシングはもはや、実現不可能です。

 なぜなら、現在、nullが返せるという謎の状況が合法だからです。(どれほどおかしいとしても!)

public Optional<Object> objMethod(){
    return null;
}

 オートボクシングを認めてしまうと、後方互換を破壊してしまいます。

 今から可能なのは、Kotlinの様に言語機能を拡張するか、後方互換の破壊か、nilとかnullに変わる単語を導入する(ミスる自信があるぞ!)か、アノテーションで処理変えるか、Optionalに暗黙に変換されるOptional2見たいな型でも用意するかぐらいでしょうか。

 

Auto-Unboxing

 Optionalのオートボクシングを認めるなら、当然、アンボクシングもあるべきです。Optional<T>→Tへのアンボクシング………の前にOptional→booleanへのアンボクシングです。アンボクシングと言うよりは、暗黙の変換ですね。

 例えば、以下の様な状況を考えると、二つ目のif文の方が読みやすいと思います。isPresentって長いし面倒くさいよ。

if(intMethod().isPresent()){
  //戻り値は特に使わない処理
}else{
}

if( intMethod() ){
  //戻り値は特に使わない処理
}else{
}

 

 ifPresentOrElseがあるだろ、この馬鹿たれ!というお叱りの声が聞こえますが、本気でそれが読みやすいと思っています?

 ifPresentOrElse(()->{},()->{})より、単純にif文の方が、私には読みやすいです。

 そして、条件式がOptionalだったら、trueの場合は_にアンボクシングした結果が格納される、みたいな構文でアンボクシングされると良いかなーとか思います。

int value = intMethod() ? _  :  0 ;
if( intMethod() ) {
  int x = _ + value;
}

 なるべくキーワードを増やさない様に_ってかいたけど、$1とか$2でもいいかもね。

 ifPresentOrElseなんかよりずっと読みやすいし、ラムダ式の変数のキャプチャとかも発生しない。

 更に推し進めて、存在演算子が導入されても良いと思います。

int value =  intMethod()? 0;

 

まとめ

 JavaもプリミティブのラッパーやString、列挙体、Cloneable、AutoCloseableなど、幾つかのクラスでは言語的に特殊処理しているのだから、Optionalは何が何でも絶対駄目という理由はないと思います。むしろ言語として特殊扱いすべきだったと思います。

 C++は難しすぎる、Javaの方が簡単と言われますが、OptionalはC++の方が簡単ですね。オートボクシングやbooleanへの変換は、C++では暗黙の型変換で普通に使えます。

 最近のJavaは言語機能の拡張に積極的ですから、早いところ、言語機能に組み込んで欲しいな。

組み込みでラムダは使って良いけど、std::functionは駄目っぽい

 例えば、組み込みでウォッチドックタイマー(WDT)を使う場面を想定しましょう。

void main()
{
  クロック設定とか;
  WDT->Prescaler = プリスケーラの設定;
  WDT->Clear     = クリア;
  WDT->Control = イネーブル;

  for(;;){
     WDT->Clear = クリア;

     何か処理;
  }
}

 無茶苦茶長い処理をする場合は処理中にクリアを挟むことはあるだろうけど、基本形はこれでいいでしょう。

 さて、どうだろう?なんとも思わない?よし、君は素晴らしい人格者だ。幸せに生きて欲しい。

 さぁ、無駄な時間を過ごす前に、ブラウザバックするなり、タブを今すぐ閉じるんだ。

 モヤモヤした何かを感じるかい?奇遇だ、私もだ。さぁ、私と一緒に地獄に堕ちようよ。

 

こう書きたい

 さて、上記の様なコードを見ると、私としては、例えばこう書きたくなります。

void main()
{
  クロック設定とか;
  WDT.period( 1ms, 100_MHz ) // WDTの基準クロックと、オーバーフロー周期
     .loop( [](){
    何か処理;
   });
}

 

 クリアだなのなんだのは隠蔽して、勝手にやって欲しい。一回のループでするべき処理だけ定義して、後は自動でやって欲しいのです。

 つまり、処理の「意味」だけに注力したいのです。レジスタとか後から読んで読めないゴミクズを、一々触りたくないのです。

(なお、1msはstd::chrono_literalsに定義してあります。 100_MHz は、ユーザー定義リテラルで自作できます。)

 

std::functionで関数を渡す

 このように、何か勝手にやって欲しい処理の間に、ユーザー定義の処理を挟むには大凡以下の方法があります。

  1. 関数を定義して、関数ポインタを渡す
  2. 関数オブジェクトを渡す
  3. ラムダ式を渡す

 先に書いた例ではラムダ式を使っていますね。なお、実際にはラムダ式は2の糖衣構文なので、基本的には二つですね。

 「C++は関数ポインタよりも、関数オブジェクトの方が最適化が効きやすい」という話もありますが、最近のコンパイラは賢いので、関数ポインタでも最適化がいけてる気がします。実際、手元にあるIARのコンパイラでリリースビルドすると、関数ポインタの最適化ができています。

 どれを使うにせよ、受け取る側(loopメソッド)はC++11以上では、std::functionで簡単に同じように受け取ることができます。

#include <functional>

void loop(std::function< void () > func);

 

 これをビルドするとどうなるのでしょうか?

 以降はWDTの例を引きずるとコードが長くなるので、以下の様な単純なコードで考えてみましょう。

#include <functional>

volatile int a=0,b=1;//レジスタを想定

void foo(std::function<void()> f)
{
  while(1)
  {
    a++;//ループの最適化をさせない
    f();
  }
}


void main()
{
  foo([](){
    int bb = b;
    b = bb * (bb + 1); // volatileなので最適化はされない
  });
}

 

 IARのコンパイラで上記のコードをデバッグビルドすると、マップファイルは以下の様になります。

Entry                       Address  Size  Type      Object
-----                       -------  ----  ----      ------
.iar.init_table$$Base         0x478         --   Gb  - Linker created -
.iar.init_table$$Limit        0x488         --   Gb  - Linker created -
?main                         0x489        Code  Gb  cmain.o [4]

(略)
std::allocator<int>::allocator(const std::allocator<int>&) [subobject]
                              0x3a1   0x8  Code  Gb  main.o [1]
(略)
operator delete (void *)
                              0x41d   0xa  Code  Gb  delop_0.o [3]
operator new (unsigned int, void *)
                              0x339   0x4  Code  Gb  main.o [1]

(略)

 newとかdeleteとかallocatorとか、組み込みの敵なメソッドがずらり。

 最適化してないからしょうが無いのかと思ったら、リリースビルドにしても、deleteや仮想テーブルが残る結果になりました。

 これでは、組み込み的にはちょっと使えません。

 ラムダ式がインライン化されて、もっとがっつり消えてくれるのかと思ってたんですけど、少なくともIAR Embedded Workbench for ARM8.40のコンパイラでは消えませんでした。

 

 そこで、もうちょっと最適化させやすくしてみます。

inline void foo(std::function<void()> f)

 fooにinlineを付けてみました。これでリリースビルドをすると、デリータ関連が消えました。fooも消えたので、fooがinline展開されて、デリータがいらなくなったのでしょう。(というか、C++において、inlineは基本的にヘッダーに関数をかける以上の意味は無いと思っていたのですが、消えてびっくり)

 しかし、仮想テーブルは残りました。std::functionという情報を消せてないようですね。(無論コンパイラによっても違うでしょうが)

 ということは、どうも組み込み的にはstd::functionが敵のようです。そうか、君もstd::vectorとかと同類の禁止薬物だったか。

 

sdt::functionを使わず、templateにしてみる

 functionがだめならば、使わなければいいのだろう?と単純にtemplate化してみました。

template<typename F>  void foo(F f)

   これを、デバッグビルドしたときのマップファイルが以下です。

Entry                       Address  Size  Type      Object
-----                       -------  ----  ----      ------
.iar.init_table$$Base          0xa4         --   Gb  - Linker created -
.iar.init_table$$Limit         0xb4         --   Gb  - Linker created -
?main                          0xb5        Code  Gb  cmain.o [3]
CSTACK$$Base            0x2000'0008         --   Gb  - Linker created -
CSTACK$$Limit           0x2000'0408         --   Gb  - Linker created -
Region$$Table$$Base            0xa4         --   Gb  - Linker created -
Region$$Table$$Limit           0xb4         --   Gb  - Linker created -
__cmain                        0xb5        Code  Gb  cmain.o [3]
__exit                        0x119  0x14  Code  Gb  exit.o [4]
__iar_data_init3               0x7d  0x28  Code  Gb  data_init.o [3]
__iar_debug_exceptions         0xd6   0x1  Data  Gb  unwind_debug.o [4]
__iar_program_start           0x12d        Code  Gb  cstartup_M.o [3]
__iar_zero_init3               0x41  0x3a  Code  Gb  zero_init3.o [3]
__low_level_init               0xd3   0x4  Code  Gb  low_level_init.o [2]
__vector_table                  0x0        Data  Gb  vector_table_M.o [3]
_call_main                     0xc1        Code  Gb  cmain.o [3]
_exit                         0x10d        Code  Gb  cexit.o [3]
_main                          0xcf        Code  Gb  cmain.o [3]
a                       0x2000'0000   0x4  Data  Gb  main.o [1]
b                       0x2000'0004   0x4  Data  Gb  main.o [1]
exit                           0xf1   0x4  Code  Gb  exit.o [2]
main                           0xe9   0x8  Code  Gb  main.o [1]
main::[lambda() (instance 1)]::operator ()() const
                               0xd9   0xc  Code  Lc  main.o [1]
void foo<main::[lambda() (instance 1)]>(main::[lambda() (instance 1)])
                               0xf5  0x18  Code  Gb  main.o [1]

 おぉ、単にmain::[lambda()(instance 1)]という型の関数オブジェクトになったお陰で、デバッグビルドだというのに、newだとかallocとかdeleteとか仮想テーブルが一切なくなっています。

 ちなみに、最適化すると、fooもラムダ式もインライン展開されて、全部消えてました。

 これなら、組み込みでもニッコリですね。

 

関数ポインタ

 テンプレートでもニッコリですが、関数ポインタにもしてみましょう。

 例の様に、キャプチャしていないラムダ式は関数ポインタに暗黙に変換できます。

volatile int a=0,b=1;//単に最適化させない為だけ

void foo(void (*f) ())
{
  while(1)
  {
    a++;// レジスタを想定して、ループの最適化をさせない
    f();
  }
}

int main()
{
  foo([](){
    int bb = b;
    b = bb * (bb + 1); // volatileなので変に最適化はされない
  });
  return 0;
}

 これを同様にIARでデバッグビルドするとこんなんができてて、微妙に8byteほどサイズが大きい。

main::[lambda() (instance 1)]::_FUN()
                               0x71   0xa  Code  Lc  main.o [1]
main::[lambda() (instance 1)]::operator ()() const
                               0x41   0xc  Code  Lc  main.o [1]

 この_FUNってなんじゃらほい?というわけで、逆アセンブル。

   0x70: 0xb580         PUSH    {R7, LR}
   0x72: 0x2000         MOVS    R0, #0
   0x74: 0xf7ff 0xffe4  BL      _ZZ4mainENKUlvE_clEv   ; 0x40
                                main::[lambda() (instance 1)]::operator ()() const
   0x78: 0xbd01         POP     {R0, PC}

 単にインスタンスを置いてラムダ式をコールしてreturnしてるだけですね。暗黙変換されるとき、こいつが渡されるようです。こういう処理が入る分、8byteほどデバッグビルドのサイズが大きくなった様子。

 なお、最適するとどっちも同じ結果になりました。

   

まとめ

 組み込みにおいては、肥大化しても良いならテンプレートを、そうでないなら関数ポインタを使いましょう。近年のコンパイラは賢いので、関数ポインタでもちゃんと最適化してくれます。

 

 無論、これは2019年現在の話であって、コンパイラが賢くなれば、std::functionでも良くなるかも知れませんが。

static_assertで比較した値を表示したい

「C++のconstexprの値がおかしいんだけど、static_assertで間違った値表示できないの?」と聞かれました。

  できねぇんだな、これが

 C++11からstatic_assertはあるというのに、何故static_assert_eqやstatic_assert_ltとかがないのだろう。謎で仕方がないです。

#include <cassert>

constexpr int x = 10;
static_assert(x == 1, "x?");

 これをg++でコンパイルすると、以下の様なエラーになります。

 xが何だかは全然分からないですね。

static assertion failed: x?
    4 | static_assert(x == 1, "x?");
      |               ~~^~~~~

 

 

 static_assertのメッセージには、ストリングリテラルしか入れられないので、動的に生成することもできないです。 C++20でconstexprにおけるメモリ確保までできる様になるんだから、そこら辺も緩和して欲しいところです。

 

組み込みの定数計算ぐらいならなんとかなるかも

 

 表示できないと言いましたが、整数値なら、裏技的に値を表示する方法があります。

constexpr int x = 10;

template<int NUMBER>
struct test
{
  static constexpr bool value=true;
  static_assert(NUMBER == 1,"test<NUMBER>");
};

static_assert(test<x>::value,"");

 

 これのg++のエラーは以下の様になります。

foo.cpp: In instantiation of 'struct test<10>':
foo.cpp:15:22:   required from here
foo.cpp:12:24: error: static assertion failed: test<NUMBER>
   12 |   static_assert(NUMBER == 1,"test<NUMBER>");
      |                 ~~~~~~~^~~~~

 そう、一度テンプレートパラメータに入れてしまうと、「foo.cpp: In instantiation of 'struct test<10>':」ここ(赤字)に値が出るんです。

 ちなみに、clang++だと、NUMBER==1じゃなくて、10==1と表示してくれます。凄いね。

 

 複雑なクラスなど使わない組み込みで、整数を計算するだけのconstexprなら、これで十分な場合も多々あると思います。

 

というわけで、まとめておいた

 一々書くのも面倒くさいと言うことで、まとめておきました。

github.com

 こんな感じで使えます。

#include "sassert.h"

// @return x * x
constexpr int pow(int x){return x  +  x;}

SASSERT_EQ(pow(10),100); // 10*10 == 100
SASSERT_NE(pow(10),20);  // 10*10 != 20
SASSERT_LT(pow(1),2);    // 1*1 < 2
SASSERT_GT(pow(10),50);  // 10*10 > 50
SASSERT_LTE(pow(10),100);// 10*10 <= 100
SASSERT_GTE(pow(10),100);// 10*10 >= 100

 エラー時の値は「nds::assert_○<型, 値, 期待値, ファイル行 >」という形で表示されるので、その言葉を探してください。IAR Embedded Workbench等で使う場合は、エラーログを右クリックし、ログのフィルタリングを無し(全て)にして表示してください。

 g++だとこんな感じ。

include/sassert.h: In instantiation of 'struct nds::assert_eq<int, 20, 100, nds::line<6> >':
foo.cpp:6:1:   required from here
include/sassert.h:54:17: error: static assertion failed: Value == Expected
   54 |   static_assert(value,"Value == Expected");
      |                 ^~~~~
include/sassert.h:31:45: error: static assertion failed: pow(10) == 100
   30 |   static_assert(::nds::assert_##op <::nds::assert_type_t<decltype(x)>,\
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   31 |                 x,y,::nds::line<__LINE__>>::value,#x message #y)
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
include/sassert.h:22:27: note: in expansion of macro '__NDS__ASSERT_BASE'
   22 | #  define SASSERT_EQ(x,y) __NDS__ASSERT_BASE(eq," == ",x,y)
      |                           ^~~~~~~~~~~~~~~~~~
foo.cpp:6:1: note: in expansion of macro 'SASSERT_EQ'
    6 | SASSERT_EQ(pow(10),100); // 10*10 == 100
      | ^~~~~~~~~~

 「include/sassert.h: In instantiation of 'struct nds::assert_eq<int, 20, 100, nds::line<6> >':」の所に、pow(10)の間違った結果が出ています。

 ライセンスはCC0なので(Unlicenseとどっちがいいの?)、適当に改変して結構です。 

 ………static_assert_eqとかC++で対応してくれないかなぁ。