y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

C++再学習メモ -- Common Lisp の視点から (C++11編)

この文章は Lisp Advent Calendar 2019 に便乗して書かれました。

この文章は何か

この文章の筆者は、C++11くらいまではなんとなく動向を追っていたが、いつの間にやら C++20 まで行っているということに気付いて cpprefjp を眺めながら勉強しようと思いたった。この文章は、その過程を記した雑多なメモで、 内容を纏める気はない 。 添え物として、ここしばらくやっていた Common Lisp との比較とか語りとかをしようという魂胆。

さきに C++ テンプレート と Common Lisp マクロとの関係を考えてみる

ふと思ったので、 誤解を恐れず C++ の言語機能 と Common Lisp のマクロとを比較してみる。

字句構造を触る 構文木を触る 静的な型で引っ掛ける 動的な型で引っ掛ける
C++ C Preprocessor 難しい。 template や関数オーバーロードなど 仮想関数
Common Lisp reader macro defmacro, symbol macro, compiler macro など 難しい。 処理系依存で出来る場合も defmethod

Lisp マクロだと構文木が触れるので、例えば statement を並べかえたりできる。 以下は、文を逆順に実行するという無意味な Common Lisp マクロだが、たぶん C++ では書くのが難しい:

(defmacro reverse-progn (&body body)
  `(progn ,@(reverse body)))
CL-USER> (reverse-progn
       (print "a")
       (print "b")
       (print "c"))
"c"
"b"
"a"
"a"

また、 Lisp マクロでは、引数に与えられた式をどの順序で評価するかも手の内にある。 ANSI CL では andor もマクロで実装されている。 一方 C++ では、 operator&&operator|| をオーバーロードすると短絡評価する機能が失われる ので、ほとんどオーバーロードされない。「関数オーバーロード」という体を守る限り、短絡評価を維持しつつオーバーロードできる日は遠そう。 *1

とはいえ、 Lisp マクロの典型的な用例 については、 C++ に既に代替手段がある。

  • with-系マクロ :: 変数を束縛する前後に処理を挟む。例えばロックの確保や解放を自動化する場合に使う。 C++ だと、 RAII イディオムのように、クラスのコンストラクタやデストラクタに処理を書いて、変数の確保と(スコープから外れることによる)解放で処理を行うパターンで代替できる。
  • do-系マクロ :: ループのためのマクロ。 C++ では、型ごとによいイテレータを定義して、 for 文で回せば代替できる。
  • def-系マクロ :: 型などを定義するためマクロ。 C++ では、そのままクラス定義や template 定義をすればよい。

こう見ると、 Lisp マクロを使ってもそんなに奇抜な構文をつくることが常日頃あるわけでもなく、ならば C++ の現状でわりと十分なのでは・・とも思ってしまう。特にラムダ式が追加された昨今の C++ なら、任意の式を lambda に突っ込んで持ち運べるので、いわゆる Scheme の call-with-系オペレータのようなものを用意すれば、十分使える気がするのだった。

一方、 C++ ではコンパイラが解決した式の静的な型で色々引っ掛けられる。関数オーバーロードが典型。 これは Common Lisp においては、標準の ANSI CL ではできない。(処理系依存の拡張で使える場合もあり、例えば SBCL では deftransform がある。)

個人的に C++ テンプレートが Lisp でも欲しいと思う機会があって、一つは複数の型の整合性を検証したい場合。 以下のよく分からない例を考える:

 #include <vector>
 #include <numeric>
 #include <iostream>

 template <typename T>
 struct Hoge {
   std::vector<T> v1_;
   std::vector<T> v2_;

   Hoge (const std::vector<T>& v1, const std::vector<T>& v2)
     : v1_(v1), v2_(v2)
   {}

   T sum() const {
     return std::accumulate(std::begin(v1_), std::end(v1_), T())
     + std::accumulate(std::begin(v2_), std::end(v2_), T()); // ここで和をとっている。
   }
 };

 int main () {
   Hoge<int> h { {1, 2}, {3, 4} };
   std::cout << h.sum() << std::endl; // => "10"
 }

上のクラス Hoge は、二つの同じ型の vector を持ち、そしてその型は + で和を取れることを前提としている。 例えば、これを std::pair で無理やりつかうと型の不整合でコンパイルエラーになる

int main () {
  // これはエラー
  Hoge<std::pair<int, int>> s { {std::make_pair(1, 2)},
                {std::make_pair(3, 4)} };
}

同じようなものを Lisp マクロで書くと、こんな感じ:

(in-package :cl-user)

(defmacro define-hoge (class-name element-type)
  `(progn
     (defclass ,class-name ()
       ((v1_ :initarg :v1 :type (array ,element-type))
        (v2_ :initarg :v2 :type (array ,element-type))))

     (defmethod sum ((obj ,class-name))
       (+ (reduce #'+ (slot-value obj 'v1_))
          (reduce #'+ (slot-value obj 'v2_))))))

(define-hoge hoge<int> int)

(let ((h (make-instance 'hoge<int> :v1 (vector 1 2) :v2 (vector 3 4))))
  (pprint (sum h))) ; => "10"

ここで、 + が使えない型 (例えば string) を使っても、 define-hoge を展開した時点ではエラーにならない。 関数を呼んだ時点で 実行時に エラーとなる。

(define-hoge hoge<string> string) ; ここではエラーにならない。

(let ((h (make-instance 'hoge<string> :v1 (vector "1" "2") :v2 (vector "3" "4"))))
  (pprint (sum h))) ; ここでやっとエラー。

C++ だとテンプレート展開時に型が検査されるが、 Common Lisp だと頑張らないとそうは出来ない。

また、 C++ テンプレートでは、そのテンプレート引数に応じて別の関数を呼びわけたりすることが出来るので、例えば std::vector<int>std::vector<double> で別の関数を呼ぶようにすることができる。

しかし、 Common Lisp では deftype で定義した型の引数で呼び分けるのはかなり難しい。 defmethod でも単純には呼びわけることができない。以下のような定義は出来ない:

;; このような記述は不可能
(defmethod foo ((bar (array double-float))
  ...)

;; こっちしかできない
(defmethod foo ((bar array))
 ...)

Common Lisp 使いが Julia言語に切り替えた理由 もその辺りにあるそうだ。

筆者としては、 Lisp マクロと C++ テンプレートとは、全く別のことをやっているので別に優劣とかはないと思っている。 構文木を触るのと、静的な型で引っ掛けるのが両方できたら最強かもしれない・・? *2

今回の範囲

C++11 から。

C++11 の言語機能を見る

一般的な機能

auto

変数宣言時、右辺の初期化子から左辺に書く型を推論してくれる機能。 今まで std::vector<string> foo = std::vector<string>(); とか書いて「なんで二回書くのか・・」と思ってたので便利。 筆者は、今でも古い Java を触る必要があることがあり、それに比べれば便利すぎると思ってやまない。

翻って Common Lisp では、変数の型を書くのが declaretype を宣言したり、 defmethod の parameter-specializer-name に型を書いたりするくらいで、何度も型を書く機会がある構文が少ない。 そもそも Common Lisp で型を書く場面は、最適化したい時や、SBCLコンパイル時に少しでも型検査 したい時、 check-type で型を確認する時くらい。ビビりの筆者は check-type を執拗に書くときことあるが、他は必要にならないとやらないのだった。

ところで:

自動変数である事を意味する記憶クラス指定子としての使用はできなくなった。

C++ の自動変数に相当する変数を Common Lisp で使うには、 dynamic-extent を宣言する必要がある。最適化するときには dynamic-extent を書いてまわることがあり面倒だなあと思いつつ、デフォルトで dynamic-extent だと lambda 書いてクロージャ作ったら変数の生存期間をすぐに越えて死んでしまうので、まあ安全側に振ってるのかなと思った。

decltype

式の型を取得し、その取得した型で変数宣言したりできる機能。 template を使うときに手書きしにくい型を書くときなどに使う模様。

さて Common Lisp では、やはり型を書く必要がないのであまり関係なさそう。しかし、例えば関数の戻り値を let する時に、「最適化するにはその型を書かないといけないけど調べるのが面倒」という時もなくはない気がする。その関数に (declaim (ftype ...)) しとけという話だが。 また、純粋に変数の型を取るには、 CLtL2の環境アクセス まで持ち出さないといけないので面倒なのであった。

範囲for文

いわゆる for-each 文のようなやつで、 for (<変数宣言> : <コンテナ>) で何でも走査できる。 これに任意のクラスを対応させるには、 begin(), end()オーバーロードして Iterator を返すようにすればよい。

Common Lisp では、このあたりの事情が結構異なる。

どんなコンテナでも走査できるオペレータは Sequence Function に挙げられているもののみで、 これらは関数。 for 文のような機能を提供するマクロは、なんとコンテナの型ごとに存在する。例えば dolist, dotimes, with-hash-table-iterator 。何でも回せる loop もあるが、 for で回すときに in (list) か, across (vector) か, fromto (数) か, being the hash-table (hash-table) か・・と使い分けないといけない。

Common Lisp では、どんなコンテナにも使える iterator を取り出し、それを使って for 文で回すということをあまりせず、このように型ごとに do-系マクロを定義してしまうということが行われる。 まあこのあたりは、 Common Lisp だとマクロで構文を増やし放題であるが故の文化なのかなとも思う。一方、 C++ 等の言語は、構文を増やすのが面倒で、よって for 文の形式は変えられないけど色々なものを回せるようにしよう・・という方向に発想が向くのだろうか。 Common Lisp でも generic-cl のように、汎用的な iterator を使うという方向に寄せてコーディングするためのライブラリもあるけれど。

また、新たなユーザ定義型を ANSI CL のコンテナにするのも難しい。例えば SBCL依存の拡張 を使う必要があったりする。 loop でユーザ定義型を回すには、一番汎用的な for = を使わないといけない。この点は、 iterateDriverを定義する 機能で低減できる。

初期化子リスト

ユーザ定義型でも、 struct や配列のように { } で初期化できるようになる。 これを行うには、 std::initializer_list を引数に取るコンストラクタを定義すればよい。

Common Lisp では・・あまり必要と思わない。 defstruct による構造体に対して #Sリーダーマクロ があるが、似たようなものを defclass した型にも作れないのか?と思ったこともある。それらもまあ #. で埋め込んじゃえばいいと思ってしまう。

一様初期化

コンストラクタを { } で呼べる。

この拡張の目的の一つは、 () を使ったコンストラクタ呼び出しが関数宣言構文とみなされてしまうことがあるが、 { } なら混同されないという点だが、 Common Lisp だとそもそも全部 () なので関係ないのであった。

一方、 戻り値の型が確定している状況で return {1, 2, 3}; のように書いて値を返したり、 foo({10, 20, 30}); のように関数を呼ぶことが出来たりする。 こういう自動的な型変換は Common Lisp にあっても便利な気もしなくもない。

右辺値参照・ムーブセマンティクス

右辺値参照により、所有権の移動を型レベルで安全に表現することが出来る。

残念ながら Common Lisp では難しそう。 個々のクラスや関数で所有権移動を行うのは簡単。しかし、それによって行われる所有権移動を型レベルで表現し、コンパイル時に検査するのは簡単ではないと思う。 (実行時に)しつこく検査することで誤魔化すことになるか、「所有権移動する場合に囲むマクロ」を用意してその中で検査するとか、そういうものが必要そう。

ラムダ式

どこでも簡単に関数オブジェクトを書けるようにする。 以前は operator()オーバーロードした構造体を定義しないといけなかったが、これがかなり面倒だったので入ってうれしい。 また、 C++ では変数の生存区間に気を配る必要があるので、何をどう取りこむかは [] の中に書かなければならない。そして取り込んだ値を変更したければ mutable を付けないといけない。

Common Lisp では昔から lambda がある。そして変数の生存区間は、 dynamic-extent 宣言しなければ無限で、とにかく全部取り込むしそれで問題ない。 ただ、 取り込んだ値が意図せず共有されてしまって頭を抱えた ことが大体年に一回ある。どんな言語でもありえると思うけれど。

noexcept

例外安全性の保証、それによる最適化のため、関数に例外を送出する可能性があるかないかを指定できるようにする機能。 C++ の例外仕様では不完全だったり、そもそも例外仕様自体が使うのがすごく難しい(と "More Effective C++" に書いてあった)ので、それを補う機能。

Common Lisp では、そもそも例外仕様なんてものがないので関係ない話。 最適化はどうなんだろう。今まで Common Lispdisassemble を眺めた感じ、 handler-bindhandler-case などを書いた近辺にしか condition 回りのコードが生成されないので、あまり関係ない気がする。 (ランタイムが頑張ってるんだろうという感覚。)

constexpr

一定の条件を満たしていれば、コンパイル時に値を計算することが出来る。

Common Lisp だと、 #. でリード時に計算したり、 (eval-when (:compile-time) ...)コンパイル時に計算したり、 defmacro でマクロ展開時に計算したりと手段がいろいろある。

ところで、 Common Lispeval-when は簡単に使えるようでハマることもままある。 一方、 C++ constexpr の使える範囲は段々と広がっているらしい。そのうち、 eval-when :compile-time と同じことを心配するようになるのだろうか・・と思ったり思わなかったりする。

nullptr

これまで 0 がヌルポインタを表わす表記だったところ、 nullptr で表わすようになる。 数値の 0 との混同が問題になってきたので導入されたらしい。

Common Lisp だと、 nil が "真理値としての偽" と "空リスト" "を両方あらわすのが似ている気がする。 この仕様、 List を走査するループを手書きする時には結構便利なのなのだけど、リストと偽とを区別しないといけない時には面倒になる。最近みたのは、 JSONLisp で読み込むライブラリが、空のJSON配列を nil にして 区別がつかないという事例。この手の問題と NULL の問題は似てそう。

C だと、 NULL が 0 であるおかげで、 NULL かどうかを if 文で簡単に書けたので便利だったのだけど、 C++ になって bool への暗黙の型変換などが整備されたことで、 0 という表現が曖昧であるという負の面が目立つようになったのだろうか・・

あと、 Common Lisp では nil という型には 値としての nil はなく、 値としての nil は null という型に所属する のは難しすぎるぞ。 年に一回くらい (check-type foo (or string nil)) とか書いてバグらせている気がする。

インライン名前空間

APIのバージョニングに使うらしい。

Common Lisp では package を使って、 同じ名前だがパッケージが異なるシンボルの use を切り替えることに相当するのだろうか・・と思ったけれど、それだと use する先を切り替えるとシンボルが衝突するので違う。 頑張って別名を定義するコードを自作しないと同じことはできなそう。

一方で、 Common Lisp だと関数を再定義しても(CL package 以外なら)お咎めがなく、関数を後から書き換えることも当然のように行われている。 なのでバージョニングにこだわらず、とにかく新しい関数定義に上書きすればよい、という発想もあり。 単一バイナリとしてビルドしても、 Lisp コードや FASL をロードするのはいつでも可能なので、必要に応じて live patch すればいいじゃないとも思うのだった。

ユーザー定義リテラル

リテラルにユーザ定義の suffix を与え、ユーザが定義した意味を持たせたリテラルを定義できる。

Common Lisp だと、プリフィックスを付けるのは reader macro で簡単だけど、suffix は難しい。 まあ文字列に suffix を付けるのなら、 " を reader macro で乗っ取れば可能と思うが、数値はかなり難しそう。 (*read-base* が 36 のとき、 36進数なので 1-9 と a-z が数値を表現することになるが、では全ての文字を reader macro で乗っ取るのか?)

まあ、 C++ のような suffix 方式を真似るのはあきらめて、素直にプリフィックスを定義するのが Common Lisp っぽい。

クラス関係の機能

関数のdefault/delete宣言 (自動定義される特殊関数の制御)

クラス定義時に暗黙的に定義されるいくつかの関数について、 default をつけてデフォルトの挙動を使用すること、もしくは delete をつけて定義しないことを明示できる。

C++ でこれが使用できる関数それぞれについて、 Common Lisp の場合に制御可能かを適当に比較してみる。

  • デフォルトコンストラクタ :: defstruct では :constructor で指定。 defclass では make-instance に相当するが、 定義は必ずされる
  • コピーコンストラクタ, ムーブコンストラクタ :: C++ の「別の構造体かオブジェクトを引数に取ってオブジェクトを初期化する」という仕組みは、 Common Lisp にはデフォルトでは用意されていない。 initialize-instance でそういう引数を定義すればできそう。 defstruct:copier オプションや copy-structure は、オブジェクトの複製を new するようなものなので少し意味が異なるか。こちらは生成するかを指定できるが defstruct 専用。
  • コピー代入演算子, ムーブ代入演算子 :: C++ のような、既存のメモリ領域を上書きするような代入演算子Common Lisp にはない(はず)。 reinitialize-instance でオブジェクトを引数にとるようにすれば出来るか。
  • デストラクタ :: ないJava のファイナライザのような仕組みも ANSI CL には用意されていない。もちろん処理系依存の実装はあり、使うために trivial-garbage のような互換性ライブラリに頼ることになる。この点は標準化されてくれないだろうか・・

移譲コンストラク

メンバ初期化リストからコンストラクタを呼ぶことにより、同じクラスの他のコンストラクタに処理を移譲できる。

Common Lisp の場合、 initialize-instancecall-next-method で別のメソッドを呼んだり、さらには :around で動作を乗っ取ってもよいので、この辺はやりたい放題。

非静的メンバ変数の初期化

非静的メンバ変数の定義時に、初期化式を記述できる。 これまで、 static でないメンバ変数はコンストラクタに初期値を書く必要があったところ、ヘッダファイルに初期値を書けるようになった。

Common Lisp の場合は、 defstruct でも defclass でもスロットの初期値を指定できるし、他にも defstruct:constructor で指定する、 defclass:default-initargs を指定するなど方法がある。

継承コンストラク

using <コンストラクタ名> と書くことにより、基底クラスで定義したコンストラクタを派生クラスでそのまま使用できる。 基底クラスのコンストラクタに引数を転送する boilerplate を減らすための拡張らしい。

Common Lisp では・・ defclass ならそもそもコンストラクタのオーバーロードがなく、 make-instance 一本。 initialize-instancecall-next-method すれば基底クラスも呼べる。 defstruct:include している場合は、たぶん転送コードを書かないといけない気がする。

override と final

override をつけて仮想メンバ関数のオーバーライドを明示的に宣言したり、 final をつけて継承をさせないことやオーバーライドを禁止したりすることを宣言したりする。

Common Lisp では、なんと ANSI CL にこの機能はない 。 そもそも、既存のメソッドに追加で defmethod して別のクラスで使えるように乗っ取ったりも出来るあたり、 C++ オーバーライドとか越えてやりたい放題なのだった。

ただ、 :metaclass を指定して継承を禁止するクラスを実現するライブラリもある: cl-abstract-classes .

明示的な型変換演算子オーバーロード

型変換する operator <型名> の前に explicit とつけることで、明示的な型変換の時にのみ呼ばれるようになる。 C++ に限らず、暗黙の型変換はその便利さ以上に、予想外の変換で墓穴を掘ることが多い気がしている。筆者はほとんど暗黙の型変換をするクラスを定義することがなかった。

一方で Common Lispdefstruct, defclass, define-condition で定義する型は、 暗黙に組込み型に変換されることはない(はず)なので、 CL には関係ない話。 型変換しようとすると、 coercechange-class を明示的に呼ぶか、 make-instance, string, character のようなコンストラクタ系の関数を呼ばないといけない。

とはいえ、組み込み型と組み込みオペレータには、一見すると暗黙の型変換に見えるものがある。 数値の Contagin Rule とか。 「○○ designator」とか。 例えば defpackage に渡すパッケージ名やシンボル名は string designator なので、実は string でも character でも symbol でもいい、みたいな奴。

これらについては、「個々のオペレータが融通を効かせている」のであって、暗黙の型変換があるというわけではないが。

friend宣言できる対象を拡張

テンプレートパラメータや型の別名も friend 宣言できるようになった。

Common Lisp では、そもそも public protected private のような アクセス修飾子がない 。 よって friend 宣言なんてものもない。

では Common Lisp は無法地帯なのかというと、「スロット名、アクセサ関数や defmethod されているシンボルが export されていなければ使わない」という紳士協定である程度のアクセス制御が担保されている。 つまり、アクセス制御的なものは、クラス単位ではなく package によって提供されていると筆者は思っている。 (もちろん、 :: を付ければ export されていないシンボルでも触り放題なので、やろうと思えばなんでも出来てしまう)

メンバ関数の左辺値/右辺値修飾

メンバ関数に、これまでの const 修飾に加えて、 &&&, const && など指定できるようになり、 *this が左辺値として呼ばれているか、右辺値として呼ばれているかなどでメンバ関数オーバーロードできる。

Common Lisp には 該当する機能はない

そもそも C++const に相当するものもない。メンバ変数への constだと defstruct の slot の read-only オプションとかはあるが、ローカル変数への const のように let した値を変更しないことを宣言する手段がない。一方で Common Lisp 書いてると setf もあんまりしない気はするが。

クラス以外の型に関する機能

スコープを持つ列挙型

enum class という、整数型への暗黙の型変換がない enum が書けるようになる。

さて、 Common Lisp には列挙型はない 。代わりに symbol を使う。大抵は keyword symbol。

C の enum のように整数型がついていると、ビットマスクを定義するときや並びかえをするときには便利だけど、その文字列表現を得るために表を定義したりすると面倒になって「symbol でいいのに」と思いはじめるので、まあ一長一短だと思う。

共用体の制限解除

union のメンバ変数として、クラスオブジェクトが持てるようになる。

一方、 Common Lisp には共用型はない 。 ただ、 C++union を使うときは、複数の型の値を入れられる variant 型のようなものを書くときだと思うが、 Common Lisp だとそれ自体が必要ない。

テンプレート関係の機能

Common Lisp にはテンプレートがないので、適当にまとめてみていく。

  • テンプレートの右山カッコ :: C++ の構文を変更してコードを書きやすくする変更。Common Lisp では、そもそも全部 ( ) なので関係がない。
  • extern template :: テンプレートのインスタンス化を抑制してコンパイル時間を減らす。複雑なマクロは展開時間がかかることもあるが、 C++ ほど問題になってない印象。
  • エイリアステンプレート :: パラメータ化した型の別名付けが出来る。 template の中で型名を let する手段が増えた感じか。
  • 可変引数テンプレート :: 任意の数のテンプレートパラメータを受け取れるようにする。 Common Lisp マクロだと &rest が取れる。
  • ローカル型と無名型を、テンプレート引数として使用することを許可 :: 制限緩和。
  • 任意の式によるSFINAE :: SFINAE が明確化されたらしい。
  • テンプレートのエクスポート機能を削除 :: export template がなくなってしまった。ところで Common Lisp では FASL に defmacro 定義を含むことができる。これって export template とどういう関係なんだろ。また、それによって展開されたコードはどういうライセンスになるのかという問題があり、そこから LLGPL が生まれたりしてるので、もし export template が生き延びても歴史が繰り替えされそう。

並行関係の機能

スレッドローカルストレージ

thread_local キーワードを指定することで、スレッドごとの静的記憶域が作られる。

Common Lisp では、 dynamic 変数に let で束縛すると thread local になるというのがよくある実装で、例えば SBCLもそうなっている。この挙動が標準化されてほしい。

ブロックスコープを持つstatic変数初期化のスレッドセーフ化

関数ローカルで定義したstatic変数の初期化がスレッドセーフにする。

C++ の関数ローカルstatic変数に相当するものは、 Common Lisp では load-time-value にあたるが、その初期化衝突が問題になったりしないのかは気になってきた。どう定義されているのか調べよう。

その他様々なユーティリティ

これもまとめてみていく。

小さな変更

定義済みマクロ、機能テストマクロ

マクロが更新された。また、C++11 の機能がサポートされているかどうかをテストするためのマクロも増えた。

閑話休題Common Lisp#+ #- では、 *features* に symbol があるかどうかを見ることは出来るが、その値を見ることは出来ない。 値を見るなら、万能 reader macro #. で symbol を評価するか、あきらめて defmacro 以降で見るかのどちらかになる。

registerキーワードを非推奨化

ところで、 C では register が付いていると & 演算子でポインタが取れなくなるのだが、その機能も C++ では無くすのだろうかと思ったが Wikipedia#register) によると:

Cのregister変数はアドレス参照(&演算子によるポインタ取得)を行えないが、C++では行えた。

ということらしく、そうなると確かに意味はない。非推奨も残当

他、制限緩和など

他はまとめてみていく。

  • 参照への参照を折りたたむ :: C++03 で「参照への参照」がエラーだったことが緩和された。
  • sizeof演算子にクラスの非静的メンバを、オブジェクトを作らずに指定できるようにする
  • テンプレート再帰回数が17回から1024回に制限緩和
  • テンプレート外でもtypenameとtemplateを付けられるようになった
  • 入れ子名の指定にグローバルスコープ::を付加することを許可
  • 宣言時に要素数を指定した配列オブジェクトの、定義時の要素数を規定 :: 未定義箇所が減った
  • POSIX用の名前空間を予約

C99互換機能

可変引数マクロ

Preprocessor マクロが任意個の引数を取れるようになるらしい。

Common Lisp だと:

  • defmacro&rest パラメータで任意個の引数を取れる。
  • reader macro だと、マクロ文字以降に現われた文字をいくつ読むかは reader macro を実装する関数に任されており、極端にはいくら読んでも構わない。ファイルの末尾まで影響する 邪悪な マクロを書くことも可能。

ただ、 dispatching macro character の中置できるパラメータは高々一個の整数に限定されている。自分で dispatching macro character のようなものを自作すればこの限りではないが。

ところで、C Preprocesssor の仕様のアレな点は mcpp に付属のドキュメントにいっぱい書いてあっておもしろくて一時期ずっと読んでた。

long long型

64ビット以上の大きさを持つ整数型。

Common Lisp だと (integer [lower-limit [upper-limit]]) と、整数型の引数で上限と下限を取れる ので表現はできる。 しかし、その上限・下限で引っかけて defmethod などできないので困ったところ。

事前定義識別子 __func__

現在いる関数名を取れる。

Common Lisp だとこれがない。 defun の早期 return も面倒で困る (が、毎回書いている):

(defun foo (n)
  (when n
    (return-from foo nil) ; ここに関数名を書くのが面倒。
  (1+ n)))

列挙子の末尾へのカンマ付加を許可

Common Lisp だと、そもそも区切りのカンマがない・・

整数に対する除算と剰余算の丸め結果を規定

これまで実装定義だった整数の除算と剰余算に対する丸め方法を、標準で規定する。

Common Lisp だと整数除算は ratio になるので、この問題はないはず。

あとはまとめて見ていく。

C++11のライブラリ更新を見る

ここからは触れたいところに触れる。

アルゴリズム

  • 指定された数の要素をコピーするstd::copy_n()を追加
  • 条件を満たす要素のみをコピーするstd::copy_if()を追加

ANSI CL には以外とこれらに対応する関数がなくて、いつも loop ~ correct で手書きしてる気がする。

並行処理

Common Lisp では、並行処理のプリミティブは Bordeaux Threads 、高級な道具立ては lparallel という感じ。 しかし、 C++ のみならず C にも thread や atomic などの概念が入った現在、ANSI CL でも標準化されてほしい。

汎用的なユーティリティ

エラー報告

OSのエラー値を扱うライブラリ<system_error>を追加

こういうのって Common Lisp にあまりない。 osicat とかだろうか。

正規表現ライブラリ

正規表現ライブラリを追加、とのこと

ANSI CL にはない。 Common Lisp では CL-PPCRE をみんな使っている印象。

乱数ライブラリ

乱数ライブラリが追加される。

ANSI CL の make-random-state が(標準では) seed を与えられないのはなぜなのか。

次回

C++17 まで行けるといいな。

*1: fexpr が再発明された上で実装される可能性が微レ存・・?

*2:Racket のマクロとか、 Template Haskell とかがそうなんだろうか