float (*(*(&e)[10])())[5]
たとえば、 「5の配列へのポインタを返す()の関数へのポインタへの10の配列への参照」型の変数を宣言する標準定義はどのくらい正確float
ですか?
@DanNissenbaum とのディスカッションに触発されました
float (*(*(&e)[10])())[5]
たとえば、 「5の配列へのポインタを返す()の関数へのポインタへの10の配列への参照」型の変数を宣言する標準定義はどのくらい正確float
ですか?
@DanNissenbaum とのディスカッションに触発されました
この投稿では、C++11 標準について言及しています。
私たちが関係している型の宣言は、C++ の文法ではsimple-declarationとして知られており、次の 2 つの形式のいずれかです (§7/1)。
decl-specifier-seq opt init-declarator-list opt ;
attribute-specifier-seq decl-specifier-seq opt init-declarator-list ;
attribute-specifier-seqは、一連の属性 ( )[[something]]
および/または配置指定子 ( alignas(something)
) です。これらは宣言の型に影響を与えないので、それらと上記の 2 つの形式の 2 番目は無視できます。
したがって、宣言の最初の部分であるdecl-specifier-seqは、宣言指定子で構成されています。static
これらには、ストレージ指定子 ( 、extern
など)、関数指定子 (inline
など)、指定子など、無視できるものも含まfriend
れます。ただし、私たちが関心を持っている宣言指定子の 1 つは、型指定子です。これには、単純な型キーワード ( char
、int
、unsigned
など)、ユーザー定義型の名前、cv 修飾子 (const
またはvolatile
)、およびその他のものを含めることができます。気にする。
例: したがって、型指定子の単なるシーケンスであるdecl-specifier-seqconst int
の簡単な例は次のとおりです。もう1つは可能性がありますunsigned int volatile
。
const volatile int int float const
「えっ、こんなのもdecl-specifier-seq なの?」と思うかもしれません。文法の規則に適合することは正しいでしょうが、セマンティック規則ではそのようなdecl-specifier-seqは許可されていません。実際には、特定の組み合わせ (それ自体以外のものとの組み合わせなど) を除いて、1 つの型指定子のみが許可され、unsigned
少なくともint
1const
つの非 cv 修飾子が必要です (§7.1.6/2-3)。
クイック クイズ(標準を参照する必要がある場合があります)
const int const
有効な宣言指定子シーケンスですか? そうでない場合、構文規則または意味規則によって許可されていませんか?
セマンティック ルールにより無効です!
const
それ自体と組み合わせることはできません。
unsigned const int
有効な宣言指定子シーケンスですか? そうでない場合、構文規則または意味規則によって許可されていませんか?
有効!がfrom を
const
分離することは問題ではありません。unsigned
int
auto const
有効な宣言指定子シーケンスですか? そうでない場合、構文規則または意味規則によって許可されていませんか?
有効!
auto
は宣言指定子ですが、C++11 でカテゴリが変更されました。以前はストレージ指定子 ( などstatic
) でしたが、現在は型指定子です。
int * const
有効な宣言指定子シーケンスですか? そうでない場合、構文規則または意味規則によって許可されていませんか?
構文規則により無効です! これは宣言の完全なタイプである可能性が非常に高い
int
ですが、宣言指定子シーケンスは だけです。宣言指定子は基本型のみを提供し、ポインター、参照、配列などの複合修飾子は提供しません。
simple-declarationの 2 番目の部分はinit -declarator-listです。これは、コンマで区切られた一連の宣言子であり、それぞれにオプションの初期化子があります (§8)。各宣言子は、単一の変数または関数をプログラムに導入します。宣言子の最も単純な形式は、導入する名前、つまりdeclarator-idです。宣言int x, y = 5;
には、宣言指定子シーケンスがあり、の後に 2 つのint
宣言子が続き、2 番目の宣言子には初期化子があります。ただし、この投稿の残りの部分では初期化子を無視します。x
y
宣言子は、変数がポインター、参照、配列、関数ポインターなどであるかどうかを指定できる宣言の一部であるため、特に複雑な構文を持つことができます。これらはすべて宣言子の一部であり、宣言ではないことに注意してください。全体として。int* x, y;
これがまさに、が 2 つのポインターを宣言しない理由です。アスタリスク*
は の宣言子のx
一部であり、 の宣言子の一部ではありませんy
。重要なルールの 1 つは、すべての宣言子が正確に 1 つのdeclarator-id (宣言している名前) を持つ必要があるということです。有効な宣言子に関する残りの規則は、宣言の型が決定されると適用されます (後で説明します)。
例: 宣言子の簡単な例は、何かへのポインターを*const p
宣言するものです。const
それが指す型は、その宣言の宣言指定子によって指定されます。より恐ろしい例は、質問で与えられたもので、(*(*(&e)[10])())[5]
ポインタを返す関数ポインタの配列への参照を宣言しています... 繰り返しますが、型の最後の部分は実際には宣言指定子によって与えられます。
このような恐ろしい宣言子に出くわすことはまずありませんが、似たような宣言子が現れることがあります。質問のような宣言を読むことができるのは便利なスキルであり、練習が必要なスキルです. 標準が宣言の型をどのように解釈するかを理解しておくと役に立ちます。
クイック クイズ(標準を参照する必要がある場合があります)
int const unsigned* const array[50];
宣言指定子と宣言子はどの部分ですか?
宣言指定子:
int const unsigned
宣言子:* const array[50]
volatile char (*fp)(float const), &r = c;
宣言指定子と宣言子はどの部分ですか?
宣言指定子:
volatile char
宣言子 #1:(*fp)(float const)
宣言子 #2:&r
宣言が宣言子指定子シーケンスと宣言子のリストで構成されていることがわかったので、宣言の型がどのように決定されるかについて考え始めることができます。たとえば、 define が「int へのポインター」であることint* p;
は明らかかもしれませんp
が、他の型の場合はそれほど明白ではありません。
複数の宣言子 (たとえば 2 つの宣言子) を持つ宣言は、特定の識別子の 2 つの宣言と見なされます。つまり、int x, *y;
は識別子x
,の宣言であり、識別子,int x
の宣言です。y
int *y
標準では、型は英語のような文 (「int へのポインター」など) として表現されます。この英語に似た形式の宣言の型の解釈は、2 つの部分で行われます。最初に、宣言指定子の型が決定されます。次に、宣言全体に再帰的な手順が適用されます。
宣言指定子シーケンスのタイプは、標準の表 10 によって決定されます。対応する指定子が任意の順序で含まれている場合に、シーケンスのタイプをリストします。したがって、たとえば、 を含む任意の順序の任意のシーケンスは、signed
「signed char」型を持ちます。宣言指定子シーケンスに現れる cv 修飾子は、型の前に追加されます。「 const signed char」型もあります。これにより、指定子の順序に関係なく、型が同じになることが保証されます。char
char signed
char const signed
クイック クイズ(標準を参照する必要がある場合があります)
宣言指定子列の型はint long const unsigned
?
"const unsigned long int"
宣言指定子列の型はchar volatile
?
「揮発性文字」
宣言指定子列の型はauto const
?
場合によります!
auto
イニシャライザから推定されます。たとえば、 と推定される場合int
、型は「const int」になります。
宣言指定子シーケンスの型がわかったので、識別子の宣言全体の型を計算できます。これは、§8.3 で定義された再帰手順を適用することによって行われます。この手順を説明するために、実行例を使用します。e
inの型を調べますfloat const (*(*(&e)[10])())[5]
。
ステップ 1最初のステップは、宣言を、が宣言指定子シーケンスでが宣言子T D
である形式に分割することです。したがって、次のようになります。T
D
T = float const
D = (*(*(&e)[10])())[5]
の型T
は、前のセクションで決定したように、もちろん「const float」です。次に、 の現在の形式に一致する §8.3 のサブセクションを探しますD
。これは §8.3.4 配列であることがわかります。これは、次の形式の宣言に適用されると述べているためT D
ですD
。
D1 [
定数式opt]
属性指定子 seq opt
私たちのD
は確かに がその形をしていD1
ます(*(*(&e)[10])())
。
宣言を想像してみてくださいT D1
( を削除しました[5]
)。
T D1 = const float (*(*(&e)[10])())
タイプは「<some stuff> T
」です。このセクションでは、識別子 の型e
が「<some stuff> 5 の配列」であると述べていますT
。ここで、<some stuff> は架空の宣言の型と同じです。したがって、型の残りの部分を解決するには、 の型を解決する必要がありT D1
ます。
これが再帰です!宣言の内側の部分の型を再帰的に計算し、すべてのステップでその一部を取り除きます。
ステップ 2したがって、前と同様に、新しい宣言を次の形式に分割しますT D
。
T = const float
D = (*(*(&e)[10])())
これは §8.3/6D
の形式に一致し( D1 )
ます。このケースは単純で、 の型T D
は単純に の型ですT D1
:
T D1 = const float *(*(&e)[10])()
ステップ 3T D
ここでこれを呼び出して、もう一度分割しましょう。
T = const float
D = *(*(&e)[10])()
これは §8.3.1 ポインターに一致し、D
は の形式* D1
です。T D1
タイプが「<some stuff> T
」の場合、タイプT D
は「<some stuff>へのポインタT
」です。したがって、次のタイプが必要ですT D1
。
T D1 = const float (*(&e)[10])()
ステップ 4それを呼び出してT D
分割します。
T = const float
D = (*(&e)[10])()
これは §8.3.5 関数に一致します。ここD
で、 は形式D1 ()
です。T D1
タイプが「<some stuff> T
」の場合、タイプT D
は「<some stuff> () を返す関数T
」です。したがって、次のタイプが必要ですT D1
。
T D1 = const float (*(&e)[10])
ステップ 5ステップ 2 で行ったのと同じルールを適用できます。ここでは、宣言子を単純に括弧で囲み、次のようにします。
T D1 = const float *(&e)[10]
ステップ 6もちろん、分割します。
T = const float
D = *(&e)[10]
§8.3.1 ポインターを再びD
フォームのと一致させ* D1
ます。T D1
タイプが「<some stuff> T
」の場合、タイプT D
は「<some stuff>へのポインタT
」です。したがって、次のタイプが必要ですT D1
。
T D1 = const float (&e)[10]
ステップ 7分割します。
T = const float
D = (&e)[10]
§8.3.4 Arrays をD
の形式で再度一致させD1 [10]
ます。T D1
タイプが「<some stuff> T
」の場合、タイプT D
は「<some stuff> array of 10 T
」です。では、T D1
の型は何ですか?
T D1 = const float (&e)
ステップ 8かっこのステップを再度適用します。
T D1 = const float &e
ステップ 9分割します。
T = const float
D = &e
ここで §8.3.2 References を照合します。ここD
で、フォームは& D1
です。T D1
タイプが「<some stuff> T
」の場合、タイプT D
は「<some stuff> reference to T
」です。では、 の型はT D1
何ですか?
T D1 = const float e
ステップ 10もちろん「T」です。このレベルには<何か>はありません。これは、§8.3/5 の基本ケース規則によって与えられます。
これで完了です。
したがって、各ステップで決定した型を見て、下の各レベルの <some stuff> を代入すると、e
inの型を決定できfloat const (*(*(&e)[10])())[5]
ます。
<some stuff> array of 5 T
│ └──────────┐
<some stuff> pointer to T
│ └────────────────────────┐
<some stuff> function of () returning T
| └──────────┐
<some stuff> pointer to T
| └───────────┐
<some stuff> array of 10 T
| └────────────┐
<some stuff> reference to T
| |
<some stuff> T
これをすべて組み合わせると、次のようになります。
reference to array of 10 pointer to function of () returning pointer to array of 5 const float
良い!これは、コンパイラが宣言の型を推測する方法を示しています。複数の宣言子がある場合、これは識別子の各宣言に適用されることに注意してください。これらを理解してみてください:
クイック クイズ(標準を参照する必要がある場合があります)
x
宣言の型は何bool **(*x)[123];
ですか?
「123 の配列へのポインタ bool へのポインタへのポインタ」
宣言のy
との型は何ですか?z
int const signed *(*y)(int), &z = i;
y
「const signed intへのポインタを返す(int)の関数へのポインタ」
z
は「const signed intへの参照」です
誰か訂正あったら教えてください!