|
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
|
🛈 | ✖ |
C++で最も厄介と言われる構文を説明し、リスト初期化({...}による初期化)の優位性と限界を議論する。
C++03より古くサイト作成者が初心者だった頃、サンプルコードMyClassクラスのc1変数を値初期化したつもりが必ずしもc1をMyClassインスタンスとして定義しない事に気付いた。それはc2変数のようなデフォルト初期化で代替できるものの、なぜそうなってしまうか暫く理解できなかった。
組み込み型でも事情は変わらないがデフォルト初期化による代替は厳密ではない。
後に教科書でc1およびi1が関数として宣言されていたことを知る(Scott Meyers, Effective STL, Boston, Addison-Wesley, 2001, pp.33-35)。スコット・メイヤーズはこれを"the most vexing parse"と呼び、本サイトは"C++最厄構文"という訳語をあてる。本記事はこれを規格に照らして再確認する。丸括弧(...)に起因する問題なのでC++11以降は波括弧{...}による初期化子で回避できるが、{...}は別の問題を顕わにして本記事はそれについても議論する。初期化に関する文脈と用語は初期化とオーバーロード(1)に従うとして各々の説明や規格参照は省く。
本記事においてオブジェクトはC++規格定義(JTC1/SC22/WG21 N4659 4.5/p1)とする。他記事のほとんどで用いる"クラスとインスタンスの総称"ではないことに留意いただきたい。本記事において関数とは断らない限り自由関数(クラスメンバ関数ではない)とする。
C++最厄構文とは教科書に説明されたもの以上でない。要は変数定義での(...)による初期化と関数宣言が特定条件で区別できず、規格はこれを関数宣言と見なすことである(N4659 9.8/p1)。本記事はこれを2つの形式に分類し、形式1は引数の無い単純な形式で、形式2は引数1個以上のより複雑な形式とする。引数とは変数定義では実引数であり、関数宣言では仮引数であり、つまり形式2は実引数と仮引数が曖昧となる構文である。
以下のf1にFooインスタンスのデフォルト初期化を伴う変数定義を意図しよう。f1は意図に反し形式1のC++最厄構文となって、仮引数が無くFoo型を返す関数宣言でその型はFoo()である。f2とf3は対比とする直接初期化を伴う変数定義で、これらは意図通りの変数定義文となる。
Foo f1();は変数定義文と関数宣言文で曖昧となって、規格により関数宣言文として扱われる(11.6/p11 Note)。関数f1は複合文(9.3)すなわちブロックスコープ(6.3.3)で宣言されて、ここでの複合文はすなわち関数本体(11.4.1/p1)に等しい。f1を関数としてコールするには定義が必要だがその定義は名前空間スコープ(6.3.6)(あるクラスのfriendであればクラススコープ(6.3.7)も可)でしか行えない。なおグローバルスコープ(global scope)もグローバル名前空間(global namespace)のスコープである事を確認しておく(6.3.6/p3)。逆に言えばブロックスコープで関数宣言できてしまうわけで、C++最厄構文はその副作用と言える。ブロックスコープの関数宣言については後に確認する。
Fooインスタンスをコンストラクタ実引数とするBarインスタンスの直接初期化を伴う変数定義を意図しよう。ここでは全てのFooインスタンスを(...)による明示的変換式(8.2.3)の結果(一時オブジェクト)で与える。明示的変換式の(...)による初期化自体は変数定義のそれのような制限は持たないことを思い出そう。b1とb2は意図通りに直接初期化を伴う変数定義となる。b3とb4はしかし意図に反して形式2のC++最厄構文となる。b3は仮引数がFoo型でBar型を返す関数宣言でその型はBar(Foo)である。b4は仮引数がFoo(*)()型でBar型を返す関数宣言でその型はBar(Foo(*)())である。b5とb6は(...)内が仮引数リストとなり得ず意図通りの変数定義となる。
b3を解説する。これが直接初期化を伴う変数定義文であればFoo(i)は直接初期化による明示的変換式であるが、関数宣言文であればFoo(i)はFoo型の仮引数iの宣言となる。後者を規格で確認する。関数宣言の(...)内は仮引数宣言節(parameter-declaration-clause)であり(11.3.5/p1)、仮引数宣言節は仮引数宣言(parameter-declaration)のリストである(p3)。仮引数宣言は宣言指定子並び(decl-specifier-seq)と宣言子(declarator)または(空かもしれない)仮想宣言子(abstract-declarator)の結合である(p3)。宣言指定子並びは宣言指定子(decl-specifier)の並び(10.1/p1)で、宣言指定子を議論の範囲で単純化すれば型定義指定子(defining-type-specifier)に等しく(p1)、型定義指定子は関数仮引数の制約(11.3.5/p11)から型指定子(10.1.7/p1)に限定され、型指定子とはcv修飾(constなど)されるかもしれない型である。宣言子/仮想宣言子を議論の範囲で単純化すれば、宣言子は変数、配列、関数の名前を必要であればポインタ演算子(ptr-operator)(*や&など)で前置するもので(11/p4)、仮想宣言子は宣言子から名前を除いたもので(11.1/p1)、ポインタ演算子の前置が無ければ仮想宣言子は空である。長々と規格を追いかけてきたが、つまりは関数宣言文でFoo型の仮引数iを宣言するならそれはFoo iであり、名前が不要ならそれはFooであり、あなたの常識に一致する。ところでiの宣言はFoo(i)でも良く(11.3/p6)、これはFoo(i)は明示的変換式であるというあなたの常識に一致しないかもしれない。結果としてBar b3(Foo(i));は変数定義文(Foo(i)は明示的変換式)と関数宣言文(Foo(i)は仮引数iの宣言)で曖昧となって、規格により関数宣言文として扱われる。
b4を解説する。これが直接初期化を伴う変数定義文であればFoo()はデフォルト初期化による明示的変換式であるが、関数宣言文であればFoo()はFoo(*)()型の仮引数の仮想宣言(名前を持たない仮引数の宣言)となる。後者を規格で確認する。Foo()が関数宣言文の仮引数に現れるとすれば、それは明示的変換式ではなく型宣言のはずなので、以下は型宣言を前提として議論を進める。Foo f1()とすればf1という名前でFoo()型の宣言(関数宣言)となることは形式1で説明した。この名前を省けばFoo()となり、すなわちこれはFoo()型の仮想宣言である。b4はこの仮想宣言を仮引数に持つBar(Foo())型の関数宣言と見なせるが関数はオブジェクトではない。初期化はオブジェクトのみが可能で仮引数は実引数でコピー初期化されるため、Foo()型の仮引数は本来なら許されない。しかし仮引数において関数型は関数ポインタ型に修正されるという特別ルールが存在し(11.3.5/p5)、つまりFoo(*)()型へ修正される。結果としてBar b4(Foo());は変数定義文(Foo()は明示的変換式)と関数宣言文(Foo()はFoo(*)()の仮想宣言)で曖昧となって、規格により関数宣言文として扱われる。
b3とb4を誤って変数定義文と見なせば実引数が異なるだけだが、正しい関数宣言文としては異なる関数型である事に注意したい。スコット・メイヤーズの教科書はC++標準ライブラリを用いて入力ストリームイテレータからコンテナを初期化したつもりが意図しない関数宣言になる場合を示した(以下、メイヤーズサンプル)。サイト作成者も何度か痛い目にあった記憶がある。
ブロックスコープの関数宣言を確認する。スコープは名前の有効範囲であり(6.3.1/p1)、ブロックは0個以上の文を{...}で囲み複合文とも呼ばれ(9.3/p1)、それ自体も文の一つなので(9/p1)ネストできる。関数宣言が定義となるためには少なくとも関数本体が必要で(11.4.1/p1)、関数本体とは=defaultなどの例外を除けば複合文である(p1)。ブロックスコープはブロック内に定義されるスコープであり、宣言文で開始してブロック終端で終了する(6.3.3/p1)。関数宣言の仮引数名として宣言されればスコープはそこから開始して最も外側のブロック(ほとんどの場合に関数本体)終端で終了する(p2)。他にif文などの丸括弧内宣言もブロックスコープとなり得る(p3,p4)。結局、ブロックスコープとは関数本体内部のネスト可能なスコープと見なして問題なく、同じ名前があれば内側のスコープが外側のそれを隠す(6.3.10/p1)。
プログラムは1つ以上の翻訳単位を結合する(6.5/p1)。翻訳単位は宣言文の集合体で(p1)、定義されたスコープ以外のスコープに宣言で導入される名前はリンケージ(linkage)を持つ(p2)。名前が他の翻訳単位のスコープあるいは同じ翻訳単位の異なるスコープから参照される場合を外部(external)リンケージ(p2.1)、同じ翻訳単位の異なるスコープからのみ参照される場合を内部(internal)リンケージと呼び(p2.2)、他の場合はリンケージをもたない(p2.3)。外部リンケージ⊃内部リンケージという関係を覚えておこう。エンティティの定義が名前空間スコープならリンケージを持ち、ブロックスコープであればリンケージを持たない(p2-p6)。無名名前空間は内部リンケージで、それに属するエンティティも内部リンケージとなる(p4)。名前のある名前空間は外部リンケージで、それに属するエンティティも変数と関数を例外として外部リンケージとなる(p3)。名前のある名前空間でconstでない変数と関数のデフォルトは外部リンケージで(p3.1)、const変数のデフォルトは内部リンケージで(p3.2)、ストレージクラス指定子(strage-class-specifier)のstatic/extern(10.1.1/p1)で内部/外部を明示指定できる(6.5/p3.1,p3.1)。あるクラスのfriend関数はクラススコープ内で定義できるが、その場合はクラスと同じスコープにあると見なす(14.3/p7)。
ブロックスコープの関数宣言とストレージクラス指定子externを持つ変数宣言は、最も内側の名前空間スコープに同じエンティティがあればそれと同じリンケージとして、そういったエンティティが無ければ外部リンケージとする(6.5/p6)。外部リンケージ⊃内部リンケージなので外部リンケージが必ずしも別翻訳単位へのリンクを伴うとは限らない。ブロックスコープでストレージクラス指定子staticとする関数宣言はできない(10.1.1/p4)。このように関数はブロックスコープで宣言できるが、定義は名前空間スコープ(あるクラスのfriendであればクラススコープも可)でしかできない(11.4.1/p2)。
まとめれば関数本体(ブロックスコープ)で関数宣言できて、C++最厄構文をもたらす結果となる。
形式1と形式2を併せてC++最厄構文となる条件をまとめる。
この文は意図に反し関数宣言文で、T()はT(*)()関数ポインタ型仮引数仮想宣言、T(x)はT型仮引数宣言である。(...)へ与える実引数の少なくとも1つがT()/T(x)以外の形式であるならば、この文は意図通りの変数定義文となる。
FoobarクラスとBazクラスのコンストラクタ仮引数を省略記号(ellipsis)のみとしている。任意数の任意型実引数をコンストラクタに渡して常にコンパイル可能で、サンプルコードの簡略化を目的とするだけのものであって他意は無い。
最初に(...)による初期化(値初期化/直接初期化)を維持するという制約を課す。()による値初期化すなわち先のサンプルコードfx0は、C++11より前に回避方法は存在しない。aを実引数リストとして(a)による直接初期化は実引数の少なくとも1つがT(x)の形式にならないようにする。サンプルコードfx3のBaz(i1)を例とすれば(Baz(i1))、Baz b1(i1)たる変数b1、Baz(i1+0)、Baz(::i1)とすれば良いが、i1+0(i1は演算可能)や::i1(i1は修飾可能)は場合によって不可能で、括弧式(Baz(i1))か変数b1とするのが間違いない。教科書もこれらを推奨していた。
制約を外せば=T(...)による初期化(明示的変換式からのコピー初期化)すれば良く、最も標準的な回避方法と言える。セマンティクスは明示的変換式T(...)の作成する一時オブジェクトを定義変数にコピーするが、規格はコンパイラ最適化による一時オブジェクト省略(RVO)を許してコピーコンストラクタはそれがコピー以外の副作用をもたらすとしても必ずしもコールされない。ただしC++17より前はセマンティクスの維持は必要でコピーコンストラクタ=deleteとするとコンパイルエラーとなる。つまり全ての場合においてC++最厄構文を回避できる方法ではない。
Tがクラス型でデフォルトコンストラクタをユーザー定義すればデフォルト初期化と値初期化は同じである。デフォルトコンストラクタがコンパイラ生成だとしても、それが自明(trivial)でなければデフォルト初期化と値初期化は同じである。つまりこれらの場合であればデフォルト初期化で値初期化を意図したC++最厄構文を回避することもできて、一番最初のサンプルコードで例を示した。ただしそれ以外の場合、デフォルト初期化と値初期化は同じとは言えない(詳しくは初期化とオーバーロード(1))。
C++11以降で初期化子に波括弧{...}を利用できるようになって、これで初期化すれば関数宣言文になりようがない。もちろんBazの初期化子も統一して{...}すべきだろう。
明示的変換式からのコピー初期化も{...}できる。C++17以降は純右辺値式からのコピー省略が保証されコピーコンストラクタ=deleteとしてもコンパイルエラーとならない。
メイヤーズサンプルも{...}で書き換えてめでたしめでたし。
リスト初期化({...}による初期化)がC++11に採用される経緯を紐解き、初期化とオーバーロード(1)を補う。C++11策定に向けた二つの主要な提案書を論拠とし、以降はそれぞれをN2215とN2640として参照する。
vをある型の値としてX型オブジェクトをvで初期化する場合に四つの構文が存在する(N2215, p.3)。
どの構文が実際に使えるかはオブジェクトの種類とvの型に依存するが、それぞれが適当な型を選べるとすれば以下にまとめられる。
| スカラー | 配列 | 集約クラス | 集約でないクラス | |
|---|---|---|---|---|
| t1 | ✔ | ✔ | ✔ | |
| t2 | ✔ | ✔ | ✔ | |
| t3 | ✔ | ✔ | ✔ | |
| t4 | ✔ | ✔ | ✔ |
t3の構文すなわち初期化リスト(initializer list)が集約(配列あるいは集約クラス型)の各要素を対応する値で初期化するのはC言語より受け継ぐ。スカラーもまた初期化リストで初期化できるが、やはりC言語より受け継ぐ(JTC1/SC22/WG14 N1570 6.7.9/p11)。このt3すなわち={v}を集約でないクラスの直接初期化へ拡張してvをコンストラクタへの実引数とする(N2215, p4)。本来はコピー初期化の文脈にある関数コールにも{v}を使えるとして、例えば関数f(X)をf({v})でコールすれば仮引数は={v}で初期される。v単一のみならず任意数の値によるリストへも拡張可能として(N2215, p5)、以降は={...}あるいは{...}と表記する。
集約でないクラスがstd::vectorのようなコンテナの場合、配列同様に{...}から各要素を初期化できれば便利で、その目的でinitializer_listクラステンプレートとその特殊化を仮引数型とする初期化リストコンストラクタ(N2215はシーケンスコンストラクタ(sequence constructor)と呼んだ)(N2215, p7)を導入する。{...}で初期化する場合に限り初期化リストコンストラクタも候補となり、それがオーバーロード選択されればコンテナの各要素を{...}の対応する値で初期化する。初期化リストコンストラクタが他のコンストラクタと曖昧となる場合は初期化リストコンストラクタを選択するが(N2215, p6)理由は後述する。
={...}の=は省くことができる(N2215, p27)。のみならずコンストラクタに実引数を与える(...)は全て{...}で置き換える事ができる(N2215, p28)。初期化リストはネストできる(N2215, p22)。
このように当初は={...}と{...}を区別せず共に直接初期化と定義したが、意図せずexplicit修飾されたコンストラクタ(explicitコンストラクタ)(変換コンストラクタでないコンストラクタ)をコールしてしまうため議論を呼ぶ(N2640, p.1)。
N2640は直接初期化とコピー初期化をコピーの必要可否で分類するのではなく、以下の様に位置付けることを主張した(N2640, pp.2-3)。
"ctor-call"が直接初期化に対応して、(...)でコンストラクタコールしていると見なせる。"conversion"がコピー初期化に対応して、受け渡される値が一意の変換元から定められる。
この観点に立てばコンストラクタは、渡される実引数が構築される値へ変換されるのではなく構築に必要な他の要素を指定するのであれば、explicitとしなければならない。
C++03以前に変換元から一時的な値を構築するコンパクトな手段は無く"ctor-call"に頼らざるを得ないが、{...}はこれをほぼ理想的に解決する(N2640, pp.4-5)。={...}あるいはf({...})における{...}を特定の"conversion"を目的として値をグループ化するものと見なし、それがクラス型の"conversion"であればユーザー定義変換のランクとして非explicitコンストラクタを必要とする。つまりC++03以前と異なり、複数の仮引数を持つコンストラクタも変換元を複数の適切の値から成るとしてユーザー定義変換になり得て、それを抑止するにはexplicitとする。初期化リストコンストラクタも同様とする。{...}が新たな初期化を導入すると見なし、それぞれの"conversion"が{...}で区分されれば複数のユーザー定義変換の連鎖が可能となる(N2640, pp.5-6)。
プログラマは{...}のネスト数で期待されるユーザー定義の暗黙的変換("conversion")数を明示できる(N2640, p.5)。暗黙的変換ではexplicitコンストラクタは候補から外される。
N2640は暗黙的変換とexplicitコンストラクタとの関係で別方法も紹介して(N2640, pp.14-15)、そちらではexplicitコンストラクタは候補に含むが選択された場合は不適格とした。N2640は前者を提案したが規格化の過程で後者が選択され、つまりサンプルコードはC++11以降でも曖昧エラーでコンパイルできない。いずれにせよ暗黙的変換でexplicitコンストラクタはコールできないという原則は変わらない。
N2215やN2640などからC++11の{...}が規格化された。
規格は初期化子{...}("ctor-call")による初期化を直接リスト初期化、初期化子={...}("conversion")による初期化をコピーリスト初期化と定義する。後者の{...}は初期化節の一つとして、変数定義以外のコピー初期化の文脈(例えば関数の実引数値渡しなど)でもターゲットをコピーリスト初期化する。コピーリスト初期化は集約で無いクラスでexplicitコンストラクタをコールできないが、これを除けば直接リスト初期化と変わらないとしてほぼ問題ない(厳密ではないが)。つまりコピーリスト初期化はコピーセマンティクスを持たずコピーコンストラクタを必要としない。これはC++17でコピー省略の保証として純右辺値式一般に拡張される。以上は初期化とオーバーロード(2)で詳説している。
N2640は{...}による縮小変換の抑止を提案し(N2640, p.2,p.6)C++最厄構文も言及する(N2640, p.7)。結論として主張するアドバイスをサイト作成者訳でここに示す(N2640, p.15)。
N2215はstd::vectorが用意する初期化リストコンストラクタとそれ以外のコンストラクタ(通常コンストラクタ)が曖昧となる場合にどうするかを議論している(N2215, p.52)。
ストロヴストルップ的議論が6ページ続いた結論をサイト作成者訳で示す(N2215, p.58)。
N2215は通常コンストラクタ側で曖昧さを解決するための新たな構文を提案せず、古い初期化子(...)で事足りるとした(N2215, p.29)。仮に{...}は初期化リストコンストラクタのみを候補として(...)は通常コンストラクタのみを候補とすれば曖昧さに関する問題を完全に回避するが、{...}の目的から大きく外れてしまう(N2215, p.53)。N2640を含めてまとめれば、{...}は初期化リストコンストラクタと通常コンストラクタを合わせて候補とするが、初期化リストコンストラクタが必ず優先される。(...)は常に通常コンストラクタを候補とする。コピー初期化の文脈ではexplicitコンストラクタをコールできず、変換コンストラクタのみがコールできる。
全ての初期化を{...}で統一するという野望は2007年に早くも潰えていた。
本記事はC++11以降の世代に属するあなたには無用かもしれない。ともかく{...}で初期化さえしていればC++最厄構文など存在しないのだから。進取に富むあなたはメイヤーズサンプルをさらにC++17以降のクラステンプレート実引数推定(CTAD)してコンパイルOKを確認するだろう。しかしそのdataがlist<int>型でなくなるのに驚くかもしれない。
CTADを維持したいあなたは仕方なくC++最厄構文を回避しつつ(...)の初期化に戻さざるを得ない。
{...}はC++最厄構文に限らず多くの古い問題を解決するが、それ自体がもたらす新たな問題を認識したい。
std::vectorを例に議論を進める。{...}の問題点とは初期化リストコンストラクタと通常コンストラクタが曖昧となって、後者を意図しながら前者を選択してしまう可能性である。両者が曖昧になるかどうかはテンプレート実引数型に依存する。vectorのコンストラクタ宣言(N4659 26.3.11.1/p2)を要素型T以外のテンプレート仮引数をデフォルトに固定するなどで簡略化して示しておく。
{...}中の縮小変換は不適格だがmingw-w64はデフォルトでその一部をエラーとせず警告表示に留め、規格もそれを許容する(4.1/p2.2)。以降のサンプルコードではこれを全てエラーと見なす#pragmaオプションを加えている。
曖昧とならない事例を最初に挙げる。クラスXが数値型からの変換コンストラクタを持たなければ、初期化リストコンストラクタと通常コンストラクタは曖昧とならない。
全ての実引数がTと特定の通常コンストラクタ仮引数型の両方に暗黙変換できる場合に、初期化リストコンストラクタと通常コンストラクタが曖昧となる。以下の場合について検討する。
vector<T>のTが数値型から暗黙変換できる場合は常に初期化リストコンストラクタとvector(size_t)/vector(size_t,const T&)が曖昧となり、初期化リストコンストラクタが選択される。
T自体が数値型でも同様となる。縮小変換でエラーとなるとしても必ず初期化リストコンストラクタが優先される。
vector<T>のTが型消去ラッパー型の場合、常に初期化リストコンストラクタとデフォルトを除く通常コンストラクタが曖昧となり、初期化リストコンストラクタが選択される。型消去ラッパー型とはあらゆる型を保持するクラスでstd::anyクラスあるいはwxWidgetsライブラリのwxVariant/wxAnyクラスが相当する。一般にはバリアント型と呼ばれるが本記事はstd::variantとの誤解を避けるため型消去ラッパー型と呼ぶ。std::variantは型消去ラッパー型ではない。
vector<T>のTがポインタ型の場合、通常なら初期化リストコンストラクタとvector(size_t)/vector(size_t,const T&)が曖昧とならない。ただし0リテラルのみポインタ型へ暗黙変換可能なので曖昧となって、初期化リストコンストラクタが選択される。
Tがvoid*の場合でvector(InputIterator,InputIterator)のInputIteratorがTのポインタ型すなわちvoid**の場合、void**はvoid*へ暗黙変換可能なので初期化リストコンストラクタとvector(InputIterator,InputIterator)が曖昧となり、初期化リストコンストラクタが選択される。
リスト初期化とCTADを同時使用すれば二つのややこしい規格が絡み合って予測の難しい結果をもたらす事は想像に難くない。最もシンプルなクラスXで検討する。あたりまえの話だが、実引数にX型インスタンスを与えない限りvector<X>は型推定できないし、整数リテラルのみを与えていればintと解釈してvector<int>(initializer_list<int>)をコールする。
vector(InputIterator,InputIterator)のCTADによる型推定は推論補助により(N4659 26.3.11.1/p2)、それを説明に不要なテンプレート仮引数を省いて示しておく。iterator_traits型特性クラスで適切に推定されるのが理解できる。
しかし常に初期化リストコンストラクタとvector(InputIterator,InputIterator)が曖昧となり初期化リストコンストラクタが選択される。
あなたは以上を十分理解した上で{...}が使えるかどうかを判断しなければならない。最も安全な回避方法は{...}を初期化リストコンストラクタをコールする場合に限定し、通常コンストラクタをコールする場合は(...)とする。それは{...}の利点を全て放棄してしまう事なのだが。縮小変換の抑止だけなら(...)も実引数2個以上で各実引数を{...}で囲めば良く、しかし1個の場合は初期化リストコンストラクタコールとなってやはり混乱の極みに落ち込む。
C++は厳密な型チェックをするがプログラマに実際の型が隠されている事が多い。ウィンドウズAPIはウィンドウ操作にHWND型ハンドル、カーネルオブジェクト操作にHANDLE型ハンドルを用いるが、それが実際は何であるかは気に留めない。C++標準ライブラリもコンテナのイテレータ型を実装定義に任せる。{...}の問題点はあなたが考える以上にあちらこちらにトラップを仕掛けている。あなたはウィンドウズプログラミングしているとしよう。HWND型とHANDLE型をstd::arrayに収めて、イテレータ範囲でstd::vectorへ{...}でコピーする。mingw-w64でHWND型は意図通りにコピーされるが、HANDLE型はそうならない。
HWND型は実際には要素ゼロの構造体__HWNDへのポインタ__HWND*で、HANDLE型はvoid*である。しかしそれぞれは識別値を直接格納して、どこかのメモリアドレスを格納しているわけではない。mingw-w64実装でarray<T>::iteratorはT*で、規格はコンテナのメモリ領域が連続であればそういった実装を許している。vector<T>::iteratorもそういった実装が多かったが最近は見なくなった。vector<HANDLE>は実際はvector<void*>で、array<HANDLE>::iteratorは実際にはvoid**であり、(*2)はvector(InputIterator,InputIterator)でなく初期化リストコンストラクタをコールする。ライブラリが供給する型や規格が実装定義とする型が、実際は何であるかを知らないとこういった問題を完全に理解できない。そういった型が将来にわたって不変である保証は無く(そうであって欲しいと願うが)、少なくともこういった状況では最も安全な回避方法を採るべきかもしれない。CTADの場合も型推定に十分な情報が実引数リストに与えられていれば事情は変わらない。
本記事はC++最厄構文の理解から出発して広く{...}初期化を議論する結果となった。C++11以降、規格は初期化子を独自の構文表記(N4659 4.3)を用いたEBNFで以下に記述する(11.6/p1)。
initializer:
brace-or-equal-initializer
( expression-list )
brace-or-equal-initializer:
= initializer-clause
braced-init-list
initializer-clause:
assignment-expression
braced-init-list
initializer-list:
initializer-clause ...opt
initializer-list , initializer-clause ...opt
braced-init-list:
{ initializer-list ,opt }
{ }
initializerすなわち初期化子はbrace-or-equal-initializerと( expression-list )すなわち(...)に分類される。brace-or-equal-initializerは= initializer-clauseとbraced-init-listすなわち{...}に分類される。{...}はinitializer-clauseすなわち初期化節にも使える。つまりは初期化子をbrace-or-equal-initializerで再定義して、その定義に漏れる(...)を後方互換に残したと言って良い。しかし{...}が初期化リストコンストラクタと通常コンストラクタに両対応するため、今でも(...)を避ける事ができない場合がある。以下にまとめる。
クラスを作るときは以下に配慮する。
CTADしたメイヤーズサンプルはstd::listを(...)で初期化し、それ以外を{...}で初期化するとして、最終回答とする。