2018-10のC++標準化委員会の文書集が公開されていたので、興味深い新機能の提案に限って紹介する。
セキュリティ上の理由でメモリの内容を破棄したい場合、コンパイラーの最適化によって意図通りのコードが吐かれないことがある。
void decrypt()
{
char password[64] ;
// パスワードを取得
get_password(password) ;
// 複合処理
// パスワードをメモリから破棄
std::memset( buffer, 0, 64 ) ;
}
このコードでは最適化の結果memsetが省略されるかもしれない。なぜなら、bufferはmemsetの後に使われていないから、memset自体が不要だとコンパイラーは判断できるからだ。
そのために、最適化によって消えずに値を消去できるライブラリを提供する。
void secure_clear( void * data, size_t size ) noexcept ;
template < typename T >
void secure_clear( T & object ) noexcept ;
この提案はさらに、secure_val<T>クラスを提案してる。secure_valはT型を保持するクラスで、デストラクターはT型をセキュアに破棄する。T型の値にアクセスする方法は関数オブジェクトを指定するもので、コピーを許さない。
void decrypt()
{
std::secure_val<char [64]> val ;
val.access([]( auto & data )
{
get_password( data ) ;
} ) ;
// 複合処理
// valのデストラクターがセキュアにメモリ上の値を廃棄
}
secure_valに似ているunique_val。これはT型をコピーせずにムーブするクラスだ。
ムーブしたあとのT型はデフォルト初期化された値になる。
ポインターやシステムリソースへのハンドルを扱うのに使える。
C++に現在提案されているモジュールの実験的な実装はFORTRANが30年間解決できなかったパフォーマンス上の課題を抱えていると警鐘を鳴らす文書。
GCCのモジュール実装では、モジュールのソースファイルをコンパイルして.nsmファイルを作成しその後そのモジュールをimportするソースファイルをコンパイルすると、該当する.nsmファイルを使う。
FORTRAN-90はモジュール機能を備えており、これは今のC++のモジュールと原理的に同じ機能を同じ実装で提供している。つまりFORTRANは30年前からモジュール機能がある。
あるモジュールをimportするソースファイルをコンパイルするためには、事前に依存するモジュールをコンパイルしなければならない。モジュールは別のモジュールをimportできる。つまりモジュールをコンパイルするには事前に依存する別のモジュールをすべてコンパイルしなければならない。
これはビルドシステムととても相性が悪い。ビルドシステムが複数のソースファイルからなるプログラムをビルドするとき、モジュールの依存関係を把握してDAGを構築し、適切な順序でモジュールをコンパイルしなければならないということだ。つまりビルドシステムはC++ソースファイルを解釈する必要がある。
IntelのFORTRANコンパイラーのマニュアルは「プログラムが依存するモジュールのファイルは事前に生成しておけ」という。Intelという超巨大で世界的な大企業ですら、莫大な利用料を支払う顧客に対して、「依存してるモジュールは事前に全部コンパイルしとけよ」ぐらいの助言しか与えられていないのだ。
著名なGNU Fortran開発者ですら、Fortranのプログラムをビルドするときは、「Makeを成功するまで十分な回数実行する」と言っている。
モジュールを正しくコンパイルするにはモジュールの依存関係の解析が必要で、そのためにはソースファイルの解釈が必要になる。Makeやninjaのような汎用的なビルドシステムにC++を解釈する機能をつけることは現実的ではないので、C++コンパイラーが依存関係を解決する機能を提供するようになるだろう。
ところで、Windowsはプログラムを起動するパフォーマンスが極めて悪い。1ソースファイルごとにC++コンパイラーを1回起動して依存関係を解決するような実装はWindowsではパフォーマンスが悪い。
不自由で低能なWindowsはプロセスの作成もスレッドの作成も遅いし、ましてやファイルの作成に至っては、i7でNVMeのSSDを積む高性能なコンピューター上で動くWindows 10がRaspberry PiとSDカード構成のRaspbianにすらパフォーマンスで負けるという信じられないほどの低能を誇っている。
Benchmarking OS primitives – Bits'n'Bites
そして、ソースファイルの一部だけを変更した後の部分的なビルドですら、モジュールの依存関係が変わるかもしれないので依存関係の解決が必要だ。
モジュールはビルド時間を削減するべきであって、ビルド時間を増やすのは本末転倒だ。
FORTRANが30年かかっても解決できていない問題は解決できそうにない。
Linuxのshared libraryやWindowsのDLLのためにエンティティをexportする属性、sharedの追加の提案。
新しいワークフロー演算子として<|と|>を追加する提案。
// 右から左
a <| b ;
// 左から右
a |> b ;
C++20にはレンジやExecutorやコルーチンやモナドが提案されているがこれらの提案は右から左、もしくは左から右といった処理の流れを記述する。例えば以下は1から始まる整数列を生成し、奇数だけをフィルターした整数列にし、その先頭から5個だけを取り出した整数列にするレンジのコードだ。
iota(1) | filter(odd) | take(5) ;
これは処理の流れがわかりにくい。すでにoperator >>はあるが、これは演算子の評価順序の関係で使えない。operator >>=なら使えるがこれは汚い。
そこでオーバーロード可能なワークフロー演算子を追加する。
iota(1) |> filter(odd) |> take(5) ;
これはほしい。
本当の条件付きコンパイル機能の提案。
constexpr ifは条件付きコンパイルではなく、条件付きテンプレート実体化抑制機能だ。そのために意味上エラーとなるコードを書くことができない。
この提案では、条件付きコンパイル機能を提供する属性により、文法上正しいが、意味上エラーとなるコードを書けるようにする。
[[ feature("key")]]
[[feature()]]はキーとなる文字列を受け取る。このキー文字列が宣言されていないか、ブロックリストに入っている場合は、その属性がある宣言とその中身がASTから取り除かれる。
利用例は以下の通り。
struct [[feature("vulkan")]] Device {
[[feature("glsl-to-spirv")]]
static Shader compile(std::filesystem::path filename);
static Shader load (std::filesystem::path spirv_file);
};
struct [[feature("direct-x")]] Device {
[[feature("hlsl-to-spirv")]]
static Shader compile (std::filesystem::path filename);
static Shader load (std::filesystem::path spirv_file);
};
いまグラフィックAPI用のライブラリを書きたいとする。このライブラリはVulkanとDirectXを両方サポートする。ただしコンパイル時の環境では、VulkanかDirectXのどちらかしか提供されていない。上のようなコードで、"vulkan"か"direct-x"のどちらかのキー文字列だけを宣言することで、2つのDeviceクラスの実装のうち、どちらか片方だけを有効にできる。無効な属性のクラスはまるごとASTから取り除かれる。
さらにこのライブラリはシェーダー言語であるGLSLやHSSLからSPIR-Vへの変換機能を提供するが、条件次第ではこの機能を提供しないことも選択できる。
文法上妥当である必要があるので、比較的穏当な条件付きコンパイルだ。#ifdefはいずれ廃止したいものだ。
ビット長を指定した整数型intN_tとuintN_tに対するユーザー定義リテラルとして、operator "" iNとoperator "" uNを追加する提案。
using namespace::literals ;
auto a = 0i16 ; // std::int16_t
auto b = 0i32 ; // std::int32_t
auto c = 0u8 ; // std::uint8_t
auto d = 0u64 ; // std::uint64_t
便利だ。
プログラムからブレイクポイントを設定できるライブラリstd::breakpointの提案。
using namespace std::literals ;
int main()
{
std::breakpoint() ;
std::cout << "hello"sv ;
std::breakpoint() ;
std::cout << "world"sv ;
std::breakpoint() ;
}
std::offsetofの提案。offsetofはマクロでstandard layout classにしか使えない。std::offsetofはstd::bit_castで実装できるが、std::bit_castはconstexprではない。std::offsetofはconstexprになるべきだが、議論が必要だ。
operator []で複数の引数を取れるようにする提案。
struct S
{
int & operator [] ( int a, int b, int c ) ;
} ;
int main()
{
S s ;
s[1,2,3] = 1 ;
}
多次元配列ライブラリが提案中だが、多次元配列へのアクセスをできるだけ直感的に書けるようにしたい。
void mainを認める提案。すでにmain関数は空のreturn文を認めていて、その場合は0を返したものとみなされる。ならばvoid mainも認めてよいはずだ。
C++にプログラムの引数の参照と、環境変数を参照、変更できるライブラリを追加する提案。C++風にイテレーターでアクセスできる。
識別子としてダラーサイン($)を認める提案。さらに、識別子の最後に限り驚嘆符(!)と疑問符を(?)を認める。
これにより"set!"や"empty?"のような関数名も使えるようになる。
$は静的リフレクションのキーワードreflexprの代わりに使えるようにすべきだという声もあるが、著者は識別子として使えるようにしたほうが有益だと主張している。
私としては$はreflexprの代わりに使いたい。jQueryのように・・・というと縁起が悪いが。
パターンマッチの提案。文法は比較的穏健。
整数の例
// Before
switch (x) {
case 0: std::cout << "got zero";
case 1: std::cout << "got one";
default: std::cout << "don't care";
}
// After
inspect (x) {
0: std::cout << "got zero";
1: std::cout << "got one";
_: std::cout << "don't care";
}
文字列の例
// Before
if (s == "foo") {
std::cout << "got foo";
} else if (s == "bar") {
std::cout << "got bar";
} else {
std::cout << "don't care";
}
// After
inspect (s) {
"foo": std::cout << "got foo";
"bar": std::cout << "got bar";
_: std::cout << "don't care";
}
tupleの例
// Before
auto&& [x, y] = p;
if (x == 0 && y == 0) {
std::cout << "on origin";
} else if (x == 0) {
std::cout << "on y-axis";
} else if (y == 0) {
std::cout << "on x-axis";
} else {
std::cout << x <<','<< y;
}
// After
inspect (p) {
[0, 0]: std::cout << "on origin";
[0, y]: std::cout << "on y-axis";
[x, 0]: std::cout << "on x-axis";
[x, y]: std::cout << x <<','<< y;
}
他にもvariantの例、ポリモーフィック型の例、式を評価する例がある。
別のパターンマッチの提案。こちらはどの関数型言語からやってきたんだというぐらい既存のC++にそぐわない異質な文法になっている。
enumの例
enum color { red, yellow, green, blue };
// Before
const Vec3 opengl_color = [&c] {
switch(c) {
case red:
return Vec3(1.0, 0.0, 0.0);
break;
case yellow:
return Vec3(1.0, 1.0, 0.0);
break;
case green:
return Vec3(0.0, 1.0, 0.0);
break;
case blue:
return Vec3(0.0, 0.0, 1.0);
break;
default:
std::abort();
}();
// After
const Vec3 opengl_color =
inspect(c) {
red => Vec3(1.0, 0.0, 0.0)
yellow => Vec3(1.0, 1.0, 0.0)
green => Vec3(0.0, 1.0, 0.0)
blue => Vec3(0.0, 0.0, 1.0)
};
あまりにもC++として異質すぎる。
クラスに対するパターンマッチの例
struct player {
std::string name;
int hitpoints;
int lives;
};
// Before
oid takeDamage(player &p) {
if(p.hitpoints == 0 && p.lives == 0)
gameOver();
else if(p.hitpoints == 0) {
p.hitpoints = 10;
p.lives--;
}
else if(p.hitpoints <= 3) {
p.hitpoints--;
messageAlmostDead();
}
else {
p.hitpoints--;
}
}
// After
void takeDamage(player &p) {
inspect(p) {
[hitpoints: 0, lives:0] => gameOver();
[hitpoints:hp@0, lives:l] => hp=10, l--;
[hitpoints:hp] if (hp <= 3) => { hp--; messageAlmostDead(); }
[hitpoints:hp] => hp--;
}
}
あまりに既存のC++の文法とは違いすぎる。
implicit constexprの提案。
constexpr関数の制約は今後ますます減っていき、コンパイル時定数式にしたい処理は今後ますます増えていく。
C++17ではlambda式のoperator ()は暗黙にconstexprだ。この結果、以下の同じように見えるコードの挙動が異なる。
int f( int x ) { return x ; }
auto g = [](int x ) { return x ; }
// Error
constexpr int a = f(0) ;
// OK
constexpr int b = g(0) ;
そこですべての関数を暗黙にconstexprにしてしまおうというのがこの提案だ。
C++のコンパイル方法を標準化しようという提案。C++の教育SGが提唱されたり、パッケージシステムも議論される中、必要な提案ではあると思うが、果たして受け入れられるだろうか。
この提案では、コンパイラーのオプション指定は+で指定する。長いオプションは++で指定する。
cpp hello.cpp ++output=hello
まず<compile>ヘッダーにコンパイラーを呼び出すライブラリが追加される。
namespace std
{
int compile(int, char * *) noexcept;
int compile(vector<string>) noexcept;
}
一つ一つの文字列がargumentとして処理される。+で始まらないargumentはソースファイル名だ。
+で始まるのはコンパイル時のオプションで、例えばヘルプメッセージの出力、出力ファイル名、デバッグといったC++コンパイラーによくあるオプションを定義している。
コンパイル方法を標準化することによって、教育や提案中のパッケージシステムで使いやすくなる。
ファイルシステムライブラリも標準C++にある今、C++ライブラリとしてのビルドシステムという不思議な概念が浮かんだ。
パッケージシステムの全容の提案、コンパイラーAPI、ビルドシステムAPI、パッケージ依存解決API、パッケージ検索APIによって構成される。
メンバーへのポインターをINVOKEのように振る舞わせる提案。
struct Foo
{
int data ;
int func( int x ) { return x ; }
} ;
int main()
{
int (Foo::*data_ptr) = &Foo::data ;
int (Foo::*func_ptr)(int) = &Foo::func ;
Foo obj ;
// Before
obj.*data_ptr = 123 ;
(obj.*func_ptr)(123) ;
// After
data_ptr(obj) = 123 ;
func_ptr(obj, 123) ;
}
ジェネリックコードで大量の特殊化を書く必要がなくなる。
符号付き整数を返すsize関数としてssizeの提案。
std::vector<int> v ;
for ( int i = 0 ; i < v.size() - 1 ; ++i )
{ }
このコードは"0u-1"を実行してしまうので意図通り動かない。ssizeがあれば、"v.ssize() - 1"と書ける。
画期的に使いやすいグラフィックライブラリ、webviewの提案の改定案。変更点としては議論が追加されている。これはHTMLとCSSをブラウザーで表示し、JavaScriptを注入できる極めて簡単なライブラリだ。