2014-01-pre-Issaquah mailingの簡易レビュー Part 2
2014-01-pre-Issaquah mailingが公開された。
前回に引き続いて、C++WGの論文集を解説していく。
N3863: Private Extension Methods
プライベート拡張メソッド(Private Extension Methods, PEM)は、privateな非virtualメンバー関数を、クラス定義の外で定義できる機能だ。
以下のようなコードを考える。
// Foo.h
class Foo
{
public :
int get_secret() ;
private :
void set_secret( int value ) ;
int secret ;
} ;
時に、このヘッダーファイルは、ユーザー側から利用される。そのようなヘッダーになぜ、privateなメンバーの情報を書いておく必要があるのか。よいクラスの条件とは、なるべく多くを隠匿することにかかっている。ユーザーにとって必要最小限のインターフェースしか提供しなければ、ユーザーにとって覚えることが少ないので使いやすくなるし、ユーザーがつまらない低級な実装の詳細にかかわらずにすむようになるし、実装の自由度も上がる。
privateなメンバーは、ユーザーとクラスの派生先からは、利用できない。したがって、ユーザー向けに提供するヘッダーに書いておく必要はない。しかし、クラス定義をユーザー向けとライブラリ作者向けに分けることは、ODRの都合上できない。
クラス定義には、ユーザーと派生クラス向けではなく、コンパイラー向けの要素も存在する。たとえば、コンパイラーはクラスのレイアウトを決定するために、すべての非staticデータメンバーやvirtual関数(vtableによる実装方法を取るコンパイラーにとっては、レイアウトの決定に関わる)の宣言を必要とする。この場合はsecretだ。
しかし、privateな非virtualであるstatic/非staticなメンバー関数はどうか。set_secretは、ユーザーからも派生クラスからも使えない。コンパイラーはクラスのレイアウトを決定するためにset_secretの存在を知る必要はない。とすれば、private non-virtual member functionは、クラス定義の外で定義してもいいのではないか。そうすれば、ユーザー側から存在を隠すことができる。
privateなメンバーをクラス定義に書かなければならない問題はいくつもある。
ライブラリ作者が、クラス定義のprivateなメンバーを追加、削除、シグネチャの変更をするたびに、そのクラスを使う翻訳単位はすべて、再コンパイルしなければならない。しかし、private非virtualメンバー関数は、完全にクラス作者の中だけで完結しているので、そのような再コンパイルは、本来必要がないのだ。
実装上の都合になるが、クラス定義に書くということは、シンボル名がその分増えるということである。シンボル名が増えるということは、shared libraryなどのシンボル名を解決して動的にロードするような機能に、余計に時間がかかるようになる。
内部リンケージを持つ名前をprivateメンバー関数で使うことができない。たとえそのprivateメンバー関数が、クラス作者の翻訳単位でしか使われないとしてもだ。
この問題は、private非virtualメンバー関数を、クラス定義の外で、追加的に宣言、定義できればよいのだ。すなわち、プライベート拡張メソッドの提案となる。そのために、privateキーワードを流用して、新たな文法を作る。
// Foo.h
struct Foo
{
int get_secret() ;
private :
int secret ;
} ;
// Foo.cpp
#include <Foo.h>
private void set_secret( int value )
{
secret = value ;
}
また、private非virtualコンストラクターも、同様に外部定義できる。
// privateコンストラクター
private Foo::Foo( int value ) : secret( value ) { }
プライベート拡張メソッド(PEM)は、staticメンバー関数にも使えるし、内部リンケージ指定もできる。ただし・・・現在提案中の文法では、色々とややこしい。
問題は、staticというキーワードは、staticメンバーの宣言にも、内部リンケージの宣言にも使われているということだ。そのため、staticキーワードは、位置に酔って意味が変わる。
- privateキーワードの後にstaticキーワードを書くと、staticメンバーになる。
- privateキーワードの前にstaticキーワードを書くと、内部リンケージを持つ。
- privateキーワードの前後にstaticキーワードを書くと、staticメンバーで内部リンケージを持つ。
// staticキーワードの利用例一覧
struct Foo{ } ;
private void Foo::f() ; // private non-static member function
private static void Foo::f() ; // private static member function
static private void Foo::f() ; // private member function with internal linkage
static private static void Foo::f() ; // private static member function with internal linkage
これはあまりにもわかりにくい。
まだC++11がC++0xと呼ばれていたドラフト段階では、一時期、内部リンケージを指定するためのstaticキーワードの使用は、deprecated扱いであった。後に覆ったが、こうしてみると、やはりstaticキーワードを内部リンケージを指定するために使うのは、間違っている気がする。
C++11では、無名namespaceが追加されたので、これを使うことにより、内部リンケージにできる。
// 無名namespaceを使って内部リンケージにする
namespace
{
private void Foo::f() ; // private non-static member function with internal linkage
private static void Foo::f() ; // private static member function with internal linkage
}
論文では、プライベート拡張メソッドを、デフォルトで内部リンケージにしてしまうという案も提示している。外部リンケージが欲しければ、明示的にexternキーワードを書くようにする。結局、多くの利用例はデフォルトが内部リンケージで問題ないだろうから。
// デフォルトで内部リンケージ案
private void Foo::f() ; // private non-static member function with internal linkage
private static void Foo::f() ; // private non-static member function with internal linkage
extern private void Foo::f() ; // private non-static member function with external linkage
extern private static void Foo::f() ; // private static member function with external linkage
あるいは、privateキーワードの利用自体を省いてしまうという案もあるが、これは賛成できない。
ところで、誰でもクラス定義の外でprivateメンバー関数を宣言できてしまうというと、アクセス指定の回避ができるかどうかが問題になるところだ。これは、通常は問題がない。なぜならば、誰でもprivateメンバー関数を宣言できるとはいえ、呼び出すことはできないからだ。
// クラスGodivaを定義しているヘッダー
#include <Godiva.h>
private auto Godiva::peeping_tom()
{
return naughty_bits ;
}
int main()
{
Godiva godiva ;
auto dirty_view = godiva.peeping_tom() ; // ill-formed、盲目となる
}
しかし、それでも規格の抜け穴を縫うような形で、プライベート拡張メソッドを使って、合法かつアクセス指定を破れるコードが存在する。
class A {
int n;
public:
A() : n(42) {}
};
template<typename T> struct X {
static decltype(T()()) t;
};
template<typename T> decltype(T()()) X<T>::t = T()();
int A::*p;
private int A::expose_private_member() { // note, not called anywhere
struct DoIt {
int operator()() {
p = &A::n;
return 0;
}
};
return X<DoIt>::t; // odr-use of X<DoIt>::t triggers instantiation
}
int main() {
A a;
return a.*p; // read private member
}
しかし、このようなコードは、うっかりと引っかかる種類のコードではない。プログラマーが意図的に行わない限り、遭遇することのないコードだ。Herb Sutterがかつて言ったように[GotW076] 、
これは実際、問題ではない。この問題は、マーフィーの法則から保護する VS マキャベリから保護するかでしかない。つまり、うっかりミスから守るか(これは言語がとてもうまくやっている)VS 意図的な悪用から守る、かだ。結局、もしプログラマーが体制を破壊したいのであれば、例1から3で示したように、必ず方法をみつけるものだ。
訳注:
マーフィーの法則=よくあることを意味するレトリック
マキャベリ=目的のために手段を選ばないことを意味するレトリック
その他、色々と反論に対する反論が書かれている。例えば、モジュール機能によりこの問題は解決するという反論には、モジュールはいつ入るかわからない。いま使える機能が欲しい。そのためには最小限度の変更で実装できる機能でなければならないと反論している。
また、クラス定義を再び開ける案も書かれている。これは、クラスのレイアウトを変化させるようなメンバーは宣言できないが、private non-virtualメンバー関数以外にも、nested typeなどを追加できる。
// クラス定義を再び開く案
class Foo { } ;
// 再び開く
private Foo
{
void f() ; // private member
int x ; // static data member
} ;
これは興味深い提案だ。
N3864: A constexpr bitwise operations library for C++
C++にビット列操作ライブラリを追加する提案。
<cmath>と<memory>に、constexprでnoexceptなビット列操作のための関数テンプレートを大量に追加する。
ビット列操作は、古くからプログラマーの実用的テクニックでもあり、遊びでもあった。ビット列操作には、実にさまざまな技法が発見されている。
よく使われるビット列操作は、ハードウェアでサポートされていることもある。しかし、CやC++では、PDP-11ですら提供しているビット列操作のインストラクションを、直接使うべき文法が存在しなかった。ビット列操作というのは、プラットフォームごとに最適な実装方法が異なる。多くのプラットフォームに対応したライブラリを実装するには、#ifdefの塊になってしまう。
信じられないことに、この2014年になっても、CとC++には、ビット列操作の文法が極めて貧弱で、標準ライブラリも存在しない。それゆえ、プログラマーは各自に独自ライブラリを作成したり、適当にネット上で検索してコピペしたりしている。
これは極めて残念なことだ。このような汎用的な処理は、共通のライブラリを作るべきなのだ。そして、標準ライブラリということになれば、たとえばコンパイラーは最適化で、プラットフォームによっては単一のインストラクションを吐いたりできる。また、複数のインストラクションを組み合わせなければならない場合でも、文脈に合わせて最適化をかけられるため、単なるインラインアセンブリ以上のことができる。
さて、ビット列操作ライブラリを付け加えるにあたって、問題がある。ビット列操作はとても多いが、いったいどの操作を標準ライブラリに含めるべきか。また、命名はどうするべきか。
どのようなビット列操作を付け加えるべきかという問題に対しては、もちろん、ハードウェアでネイティブにサポートされているビット列操作も考慮するが、それ以外にも、たとえ現実のハードウェアでネイティブにサポートするものがなくても、よく使われる有名で基礎的なビット列操作も含める方針だ。
命名方法はバイク小屋議論となるので難しい。たとえばビット列の末尾に連続するゼロを数え上げる(Count Trailing Zeros)ビット列操作を考えよう。
まず、英語の頭文字をとった、std::ctz()が考えられる。これはアセンブラーで使われているニーモニックによく似たスタイルの命名法で、昔まだ識別子の長さがそうとう短く制限されていた時代の名残である。これは曖昧でわかりにくいが、タイプ数を節約できる。
あるいは、std::count_trailing_zeroes()だろうか。これは、たしかにわかりやすいし、曖昧性もないが、長ったらしいし、複雑な式の中で使うのは難しい。
提案された論文での命名規則は、ニーモニックよりの改良案となっている。
すべての関数名は動詞から始まる。動詞は、少なくとも3文字を使って略字とする。そのため、std::ctz(Count Trailing Zeroes)ではなく、cnttz(CouNT Trailing Zeros)となる。
さらに、0と1に対しては、0と1を用いる。ZやOや、その他の文字は用いない。これにより、cntt0(CouNT Trailing 0)となる。
名詞は、一貫性を持って再利用できる場合、一語一文字で略される。たとえば、以下のような名詞を再利用する。
- t0: trailing 0s
- t1: trailing 1s
- l0: trailing 0s
- l1: trailing 1s
- ls1b: least significant 1 bit
- ms1b: most significant 0 bit
- ls0b: least significant 1 bit
- ms0b: most significant 0 bit
ここ何年も、純粋に規格漬けになっていた筆者としては、std::count_trailing_zeros()の方が、曖昧性なくわかりやすくていいのではないかと思う。識別子の長さは、clang-completeのような名前補完ツールを使えばいいのだ。
ちなみに、ドワンゴ社内では、std::ctz()やstd::cntt0()で何の問題もないとする人間ばかりだった。思うに、これは慣れの問題だと思う。ドワンゴ社内のプログラマーというのは、ビット列操作を自前で書くし、アセンブリ言語のニーモニックを日常扱う人間ばかりなので、このような略字に慣れているのだろう。あるいは、現場のプログラマーは、補完ツールが発達した今でも、タイプ数の大小、ソースコード内の一行に収まる情報量という物理的な要素を強く意識するものなのかも知れない。
筆者は、アセンブリ言語は10年ほどやっていないし、それにやっていた時も、マクロを多用して暗号的にわかりづらいニーモニックをまともな名前にしていたし、まだ少しは読めるDSLを実装していた。そういえば昔、自作したISAバス接続の基板をつなげて、MS-DOSから入出力ポートを直接叩いて自作の基板と通信する課題で、言語は自由だったので、他の皆がアセンブリで苦労して書いている中、筆者は16bit DOS用のバイナリが吐けるDigital Mars C++コンパイラーを使って、悠々とC++で高級な入出力ポート操作用のライブラリを使って、入出力ポートを叩いていたものだ。今思えば、筆者は変わり者だったのかも知れない。
それにしてもこの論文は、ドワンゴ社内でも特に食いつきがすごかった。筆者の周辺の人間は皆、論文やGitHubに上がっているリファレンス実装を積極的に読み、これは良いの、あれは悪いの、それは疑問と盛り上がっていた。やはり、ビット列操作に多少なりとも興味を示さないプログラマーは、本物のプログラマーとはいえないだろうし、それに、ドワンゴ社内の仕事でもビット列操作は必要であり、多用されているからだ。
このような現場の知見を得られるというのは、もう何年も規格ばかりやっていた筆者にとっては、なかなか悪くない環境だ。C++標準化委員会としても、感想すらやってこないものに対応できるわけがない。何が良い、何が悪い、何がわからないということは、言わなければ伝わらない。C++標準化委員会にエスパー能力を求めてもらっては困る。本来、日本のC++を扱う企業は、もっとC++標準化委員会に知見を上げるべきなのだ。C++を使っているのに、不満点を言わなければ、いつまでたっても不満は解消されない。社内に実績あるライブラリがあれば、標準ライブラリとして提案しなければ、いつまでたってもC++の標準ライブラリは貧弱なままなのだ。日本の企業はもっと積極的にC++規格に関わる必要があると思った次第。
[PDFに改良案はない] N3865: More Improvements to std::future<T>
futureを更に改良する提案。
現在のfutureは、値と例外を同時に扱っている。
#include <thread>
#include <future>
int main()
{
auto f = std::async( []() -> int
{
throw 0 ;
return 0 ;
} ) ;
// 例外が投げられる
int result = f.get() ;
}
future/promise/asyncは、新しくC++11に追加された標準ライブラリexception_ptrを使って例外を束縛し、スレッドを超えて例外を伝えることができる。問題は、もしasyncに渡した関数オブジェクトが例外を外に投げた場合、futureのget()は、例外を投げる。
futureのgetは、値と例外を一緒くたにあつかっている。これは様々な点で不便だ。たとえば、例外を投げてほしくない場合、直ちにtry { } catch( ... ) { }などで囲まなければならないし、その場合でも、やはり例外を投げているわけで、無駄なコストが発生する。
また、futureが持っている例外をexception_ptrの形で欲しい場合でも、やはり一度投げさせてtry catchで受け止めなければならない。なんという無駄だろうか。
そもそも、future自体は、値を持っているのか例外を持っているのか、知っているわけだ。futureが今、readyで、しかも値を持っているのかどうか調べることのできるメンバー関数を追加してはどうか。
そして、値を取得するメンバー関数、get_valueと、exception_ptrを取得するメンバー関数、get_exceptionも追加する。
std::future<int> f = ...
if( f.has_value() )
{
// 値を持っている
int result = f.get_value
}
else
{
// 例外を持っている。
std::exception_ptr ptr = f.get_exception_ptr() ;
}
ところで、このようにhas_valueでしらべてからget_valueを使うのは面倒だ。もしfutureが値を持っておらず例外を持っている場合に、実引数に指定した値を返すちょっとした関数が欲しい。それがvalue_orだ。
// value_orの例
x = f.value_or(v) ;
このコードは、以下のように書くのと等しい。
// 同等のコード
x = (f.has_value()) ? f.get_value() : v ;
nextは、thenと似ているが、futureに値がセットされている時のみ関数オブジェクトが呼ばれる。
// nextの例
f.next( []( auto f )
{
auto value = f.get() ; // 必ず値がセットされている
}) ;
recoverは、thenと似ているが、futureに例外がセットされている時のみ関数オブジェクトが呼ばれる。recoverの引数は、std::exception_ptrとなる
// recoverの例
f.recover([]( std::exception_ptr ptr )
{
// ptrは例外を束縛している
}) ;
futureに例外がセットされている時、フォールバックとしての値を使いたい時がよくある。fallback_toは、そのような利用例のためのメンバー関数だ。
// fallback_toの例
x = f.fallback_to(v) ;
これを真面目に書くと、以下のようになる。
// 真面目に書いた例
x = f.then( [&]( auto f )
{
return f.value_or(v) ;
}) ;
make_exceptional_futureは、例外がセット済みのfutureを作れる。make_ready_futureの例外版といったところだ。
// make_exceptional_futureの例
auto f = std::make_exceptional_future<int>( 123 ) ;
たしかに、値と例外が一緒くたになっているのは不便だ。これはなかなかいい提案だ。
N3866: Invocation type traits (Rev. 2)
C++に新たなtype traits、invocation_typeを追加する提案。
invocation_typeはメタ関数で、メタ関数引数は関数型、関数型の戻り値の型にクラスを、仮引数リストに、実引数の型リストを与えると、実際に呼び出す関数型(あるいはメンバー関数型)を返す。
// invocation_typeの例
struct C
{
int operator() ( double, int ) ;
int operator() ( double, double ) ;
} ;
// int ( double, int )
using type1 = std::invocation_type< C ( int, int ) >::type ;
// int ( double, double )
using type1 = std::invocation_type< C ( double, double ) >::type ;
invocation_type< Fn( ArgTypes ... ) >は、Fnを実引数ArgTypesで関数呼び出ししたときに、どのような型の関数が選ばれるかを、::typeで返すメタ関数である。result_ofは呼び出した結果の型を返すが、invocation_typeは、呼び出される関数の型を返す。これにより、呼び出される最適関数のformal parameterを得ることができる。
なるほど、たしかに面白いが、いったいどのように利用するのか。これはMore Perfect Forwardingを実装するのに使う。
たとえば、以下のようなコードは、より完全な転送ができないので、コンパイルエラーになる。
// More Perfect Forwardingが必要な例
void f( int & i ) ;
int i ;
int main()
{
f( i ) ; // well-formed
std::thread t( f, i ) ; // ill-formed
}
見ての通り、より完全な転送が失敗している。このコードをうまく通るようにするには、threadの実装で、最適関数に選ばられる関数の実際の仮引数の型を知る必要がある。そのため、invocation_typeが必要になる。
実は、invocation_typeは、より直接的な、raw_invocation_typeのラッパーである。invocation_typeは、もし第一引数がメンバーへのポインターであり、第二引数がrvalueリファレンスである場合、decayを適用してリファレンスを外す。これは、すぐに寿命が尽きる一次オブジェクトを束縛してしまうのを防ぐためである。raw_invocation_typeは、そのような処理を行わない、生の関数型を返す。
さらに、提案では、function_call_operatorを提供している。これは、::valueで、オーバーロード解決で選ばれたメンバーへのポインターを返すメタ関数である。
// function_call_operatorの例
struct C
{
void operator ()() ;
void operator ()( int ) ;
} ;
int main()
{
auto fco = std::function_call_operator< C ( int ) >::value ;
C c ;
c.*fco(0) ; // call C::operator()(int)
}
これはぜひとも入って欲しい。
N3867: Specialization and namespaces (Rev. 2)
別の名前空間からテンプレートの明示的特殊化と部分的特殊化ができる提案。
ユーザーがテンプレートを明示的に特殊化することを前提とした設計のライブラリがある。たとえば、std::hashだ。このテンプレートは、ユーザー側がユーザー定義型に対するハッシュの計算方法を指定するために、ユーザーに明示的に特殊化させることを想定している。
問題は、その特殊化には、同じ名前空間であることを必要とするので、面倒なのだ。クラスを定義したら、名前空間を閉じて、目的の名前空間を開き、また閉じて、さて、また元の名前空間を開いて、続きを記述しなければならない。
namespace A {
namespace B {
/* ... */
class C {
/* ... */
};
// hashの明示的特殊化を宣言したい
} // Bを閉じる
} // Aを閉じる
// stdを開く
namespace std {
template<>
struct hash<A::B::C> {
size_t operator()(A::B::C const &c) { /* ... */ }
};
} // stdを閉じる
namespace A { /* さっきまで使っていた名前空間を開き直す */
namespace B {
/* ... */
}
}
こんなコードは書きたくない。以下のように書きたいものだ。
namespace A {
namespace B {
/* ... */
class C {
/* ... */
};
template<>
struct ::std::hash<C> {
std::size_t operator()(C const &c) { /* ... */ }
};
/* ... */
}
}
このように書けるようにする提案。
N3869: Extending shared_ptr to Support Arrays, Revision 1
shared_ptrで、配列を直接サポートする提案。これまでは、カスタムデリーターを渡してやれば、配列を扱うこともできた。しかし、もっと直接的に、
// こんなこといいな、できたらいいな
std::shared_ptr< int [] > ptr{ new int [10] } ;
こう書きたいはずだ。すなわち、shared_ptr<T[]>が、直接サポートされていて欲しい。これをサポートする提案。
論文では、shared_ptr<T[N]>のサポートも提案している。unique_ptrでこれが取り除かれたのは誤りであるとし、むしろunique_ptrにもT[N]を追加すべきだと主張している。
また、論文では、reinterpret_pointer_castという関数テンプレートを提案している。これは、shared_ptr<U>からshared_ptr<T>に変換するものだ。論文はこれをあまり強く主張しておらず、もし削らなければならないのであれば、必要な犠牲として受け入れるとしている。
実装は簡単で、既存のshared_ptrのコードを劇的に変更する必要はない。
Boost 1.53のshared_ptrは、この提案通りに配列をサポートしていて、テストやドキュメントも揃っている。
N3870: Extending make_shared to Support Arrays, Revision 1
N3869と関係していて、make_shardを配列に対応させる提案。
前回の論文、N3641との違いは、寄せられた意見を元に、オーバーロードを極端に削った。今の提案がサポートしているのは、2パターンの利用例だ。
// shared_ptrで配列を確保する例
// 値初期化されたN個のT型の配列、new U[N]()と等しい
// std::shared_ptr< int [ ] >
auto p1 = std::make_shared<int>( 10 ) ;
// std::shared_ptr< int [10] >
auto p2 = std::make_shared< int [10] >( ) ;
// 指定した値で初期化するもの、std::vector<U>コンストラクターと等しい
// std::shared_ptr<int [] >
auto p3 = std::make_shared<int>( 10, 0 ) ;
// std::make_shared< int [10] >
auto p4 = std::make_shared< int [10] >( 0 ) ;
以前の提案では、二次元配列の確保などの、やたらと複雑なオーバーロードが存在した。
N3871: Proposal to Add Decimal Floating Point Support to C++ (revision 2)
十進浮動小数点数をサポートする提案。decimal32, decimal64, decimal128という名前のクラスが提案されている。
C++の標準規格では、浮動小数点数がどのように表現されているかを定めていない。したがって、規格上は十進浮動小数点数で実装することも可能であるが、既存のC++実装はすべて、速度を重視して、浮動小数点数の内部表現は二進数にしている。
十進浮動小数点数の利点は、10進数を正確に表現できることである。たとえば、十進浮動小数点数を使えば、二進浮動小数点数では正確に表現できなかった、10進数の0.1が、正確に表現できるようになる。
論文では、C++03向けに書かれた十進浮動小数点数のTechnical Reportを参照して、C++14用に修正している。この提案が規格入りすれば、C++17では、十進浮動小数点数のサポートは、規格上、必須になる。
C++03向けのTRをC++17向けに書きなおすにあたってなされた変更も興味深い。
まず、10新浮動小数点数型を、Standard layout typeかつPODにするために、明示的にdefault化された宣言を使う。
また、long longへの変換関数は、暗黙的でないほうがいいだろうということで、explicitになった。
// explicitな変換関数になった
decimal32 d = 123.0 ; // OK, doubleからdecimal32は暗黙に変換できる
long long i1 = d ; // ill-formed、decimal32からlong longは暗黙に変換できない
long long i2 = static_cast<long long>(d) ; // OK
また、十進浮動小数点数はクラスではあるが、実装により組込型扱いすることを認めるために、finalとなる。つまり、派生することができない。
ちなみに、ドワンゴの業務で十進浮動小数点数の需要があるかどうかを聞いてみたところ、「あんまりなさそうな」という答えが返ってきた。
ドワンゴ広告
この記事もドワンゴの勤務時間中に書いた。ドワンゴはC++の啓蒙活動を進めている。
ドワンゴは仕事のできるC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0