C++再学習メモ -- Common Lisp の視点から (C++11編)
この文章は Lisp Advent Calendar 2019 に便乗して書かれました。
この文章は何か
この文章の筆者は、C++11くらいまではなんとなく動向を追っていたが、いつの間にやら C++20 まで行っているということに気付いて cpprefjp を眺めながら勉強しようと思いたった。この文章は、その過程を記した雑多なメモで、 内容を纏める気はない 。 添え物として、ここしばらくやっていた Common Lisp との比較とか語りとかをしようという魂胆。
- この文章は何か
- さきに C++ テンプレート と Common Lisp マクロとの関係を考えてみる
- 今回の範囲
- C++11 の言語機能を見る
- C++11のライブラリ更新を見る
- 次回
さきに 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 では and
や or
もマクロで実装されている。
一方 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 では、変数の型を書くのが declare
で type
を宣言したり、 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) か, from
と to
(数) か, 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
=
を使わないといけない。この点は、 iterate の Driverを定義する 機能で低減できる。
初期化子リスト
ユーザ定義型でも、 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 Lisp の disassemble
を眺めた感じ、 handler-bind
や handler-case
などを書いた近辺にしか condition 回りのコードが生成されないので、あまり関係ない気がする。
(ランタイムが頑張ってるんだろうという感覚。)
constexpr
一定の条件を満たしていれば、コンパイル時に値を計算することが出来る。
Common Lisp だと、 #.
でリード時に計算したり、 (eval-when (:compile-time) ...)
でコンパイル時に計算したり、 defmacro
でマクロ展開時に計算したりと手段がいろいろある。
ところで、 Common Lisp の eval-when
は簡単に使えるようでハマることもままある。
一方、 C++ constexpr
の使える範囲は段々と広がっているらしい。そのうち、 eval-when
:compile-time
と同じことを心配するようになるのだろうか・・と思ったり思わなかったりする。
nullptr
これまで 0
がヌルポインタを表わす表記だったところ、 nullptr
で表わすようになる。
数値の 0 との混同が問題になってきたので導入されたらしい。
Common Lisp だと、 nil
が "真理値としての偽" と "空リスト" "を両方あらわすのが似ている気がする。
この仕様、 List を走査するループを手書きする時には結構便利なのなのだけど、リストと偽とを区別しないといけない時には面倒になる。最近みたのは、 JSON を Lisp で読み込むライブラリが、空の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-instance
で call-next-method
で別のメソッドを呼んだり、さらには :around
で動作を乗っ取ってもよいので、この辺はやりたい放題。
非静的メンバ変数の初期化
非静的メンバ変数の定義時に、初期化式を記述できる。
これまで、 static
でないメンバ変数はコンストラクタに初期値を書く必要があったところ、ヘッダファイルに初期値を書けるようになった。
Common Lisp の場合は、 defstruct
でも defclass
でもスロットの初期値を指定できるし、他にも defstruct
の :constructor
で指定する、 defclass
の :default-initargs
を指定するなど方法がある。
継承コンストラクタ
using <コンストラクタ名>
と書くことにより、基底クラスで定義したコンストラクタを派生クラスでそのまま使用できる。
基底クラスのコンストラクタに引数を転送する boilerplate を減らすための拡張らしい。
Common Lisp では・・ defclass
ならそもそもコンストラクタのオーバーロードがなく、 make-instance
一本。 initialize-instance
で call-next-method
すれば基底クラスも呼べる。
defstruct
で :include
している場合は、たぶん転送コードを書かないといけない気がする。
override と final
override
をつけて仮想メンバ関数のオーバーライドを明示的に宣言したり、 final
をつけて継承をさせないことやオーバーライドを禁止したりすることを宣言したりする。
Common Lisp では、なんと ANSI CL にこの機能はない 。
そもそも、既存のメソッドに追加で defmethod
して別のクラスで使えるように乗っ取ったりも出来るあたり、 C++ オーバーライドとか越えてやりたい放題なのだった。
ただ、 :metaclass
を指定して継承を禁止するクラスを実現するライブラリもある: cl-abstract-classes .
明示的な型変換演算子のオーバーロード
型変換する operator <型名>
の前に explicit
とつけることで、明示的な型変換の時にのみ呼ばれるようになる。
C++ に限らず、暗黙の型変換はその便利さ以上に、予想外の変換で墓穴を掘ることが多い気がしている。筆者はほとんど暗黙の型変換をするクラスを定義することがなかった。
一方で Common Lisp 。defstruct
, defclass
, define-condition
で定義する型は、 暗黙に組込み型に変換されることはない(はず)なので、 CL には関係ない話。
型変換しようとすると、 coerce
や change-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
にあたるが、その初期化衝突が問題になったりしないのかは気になってきた。どう定義されているのか調べよう。
その他様々なユーティリティ
これもまとめてみていく。
- 戻り値の型を後置する関数宣言構文 :: 戻り値の型を後ろに書けるようにする。
decltype
で導出するための機能。 - コンパイル時アサート :: コンパイル時に条件式が真であることを表明する。 Common Lisp では
(eval-when (:compile-toplevel) (assert ...))
で出来そう。 - 生文字列リテラル :: 'R' をつけると、エスケープシーケンスを無視する文字列を書ける。 Common Lisp にはないが、ダブルクオートのエスケープをさぼるのにシンボルを使う という技があるそうだ。
- char16_tとchar32_t :: UTF-16とUTF-32の文字型。 Common Lisp だと
base-char
extended-char
という区別はあるものの、ほとんどはcharacter
でまとめて扱ってしまうので意識しない。一方、 Unicode サポートとかは処理系依存なので大変。 SBCL にはある - UTF-8文字列リテラル :: charの文字列をUTF-8エンコーディングするプレフィックス。これも事情は上と同じ。
- 属性構文 ::
[[ ]]
構文でいろいろ指定できる。 Common Lisp でのdeclare
とdeclaim
に相当すると思うが、もっと指定できるものが増えて欲しいところ。 - alignas, alignof :: アライメントを指定, 取得する。 ANSI CL にはない。
小さな変更
定義済みマクロ、機能テストマクロ
マクロが更新された。また、C++11 の機能がサポートされているかどうかをテストするためのマクロも増えた。
閑話休題、 Common Lisp の #+
#-
では、 *features*
に symbol があるかどうかを見ることは出来るが、その値を見ることは出来ない。
値を見るなら、万能 reader macro #.
で symbol を評価するか、あきらめて defmacro
以降で見るかのどちらかになる。
registerキーワードを非推奨化
ところで、 C では register
が付いていると &
演算子でポインタが取れなくなるのだが、その機能も C++ では無くすのだろうかと思ったが Wikipedia#register) によると:
ということらしく、そうなると確かに意味はない。非推奨も残当。
他、制限緩和など
他はまとめてみていく。
- 参照への参照を折りたたむ :: 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 になるので、この問題はないはず。
他
あとはまとめて見ていく。
- Pragma演算子 :: Preprocessor マクロで処理系依存の機能を呼ぶための拡張。
- 定義済みマクロが更新された。
- 文字列リテラルとワイド文字列リテラルの結合 :: ワイド文字列定数として結合するらしい。 Common Lisp には ワイド文字列リテラルに相当するものがない。
C++11のライブラリ更新を見る
ここからは触れたいところに触れる。
アルゴリズム
- 指定された数の要素をコピーするstd::copy_n()を追加
- 条件を満たす要素のみをコピーするstd::copy_if()を追加
ANSI CL には以外とこれらに対応する関数がなくて、いつも loop
~ correct
で手書きしてる気がする。
並行処理
Common Lisp では、並行処理のプリミティブは Bordeaux Threads 、高級な道具立ては lparallel という感じ。 しかし、 C++ のみならず C にも thread や atomic などの概念が入った現在、ANSI CL でも標準化されてほしい。
汎用的なユーティリティ
- C++ の
std::swap()
関数に相当するのは、 Common Lisp だとrotatef
の二引数機能だけど、名前から類推できないよね・・ - 時間ユーティリティライブラリ
を追加とのこと。 Common Lisp では、 ANSI CL で規定されているもの はあまり多くなく、 local-time などに頼ることも多い。
エラー報告
OSのエラー値を扱うライブラリ<system_error>を追加
こういうのって Common Lisp にあまりない。 osicat とかだろうか。
正規表現ライブラリ
正規表現ライブラリ
ANSI CL にはない。 Common Lisp では CL-PPCRE をみんな使っている印象。
乱数ライブラリ
乱数ライブラリ
ANSI CL の make-random-state
が(標準では) seed を与えられないのはなぜなのか。
次回
C++17 まで行けるといいな。