1. ホーム
  2. c++

[解決済み] 演算子の優先順位以外で、余分な括弧が効果を発揮するのはどのような場合ですか?

2023-02-21 09:03:13

質問

C++の括弧は、関数呼び出しや演算子の優先順位を上書きするための式のグループ化など、多くの場所で使用されています。 違法な余分な括弧は別として (関数呼び出しの引数リストなど) を除けば、C++ の一般的なルールとして 余分な括弧は決して損をしない :

5.1 一次式 [expr.prim] について

5.1.1 一般的なもの [expr.prim.general].

6 括弧で囲まれた式は、型と値が同じである一次式です。 が同じである一次式です。括弧の有無は が存在しても,その式がlvalueであるかどうかには影響しない。 括弧で囲まれた式は,囲んだ式と全く同じ文脈で使うことができる。 括弧で囲まれた式は,囲まれた式が使用できるのと全く同じ文脈で,同じ意味合いで使用することができます。 と同じ意味で使うことができます。 ただし、特に指定がない限り .

質問 : 基本的な演算子の優先順位を上書きする以外に、どのような状況で余分な括弧がC++プログラムの意味を変えるのでしょうか?

注意 : の制限を考えています。 ポインタ・トゥ・メンバ 構文を &qualified-id はスコープ外になるように括弧なしで は構文を制限します。 というように、異なる意味を持つ二つの構文を許容するのではありません。同様に プリプロセッサマクロ定義内の括弧は も不要な演算子の優先順位から保護します。

どのように解決するのですか?

TL;DR

余分な括弧は、以下の文脈でC++プログラムの意味を変えます。

  • 引数依存の名前検索を防止する
  • リストコンテキストでカンマ演算子を有効にする
  • 厄介な解析の曖昧さ解消
  • での参照性の推論 decltype 表現
  • プリプロセッサーのマクロエラーの防止

引数依存の名前ルックアップの防止

規格の附属書Aで詳しく説明されているように post-fix expression という形式の (expression)primary expression ではありませんが id-expression でもなく、したがって unqualified-id . これは、引数依存の名前検索が、以下の形式の関数呼び出しで防がれることを意味します。 (fun)(arg) という形式の関数呼び出しでは、従来の形式である fun(arg) .

3.4.2 引数依存の名前検索 [basic.lookup.argdep].

1 いつ が、関数呼び出しの後置表現(5.2.2)が 非限定ID である場合、通常の非限定検索 (3.4.1) では考慮されない他の名前空間が検索されることがあります。 は、通常の非限定検索 (3.4.1) では考慮されなかった他の名前空間が検索されるかもしれません。 名前空間スコープの友人関数または関数テンプレートの宣言 (11.3)を見つけることができるかもしれません。検索に対するこれらの変更は 検索に対するこれらの変更は,引数の型に依存します(また,テンプレート 引数の場合、テンプレート引数の名前空間)に依存します。[ 例

namespace N {
    struct S { };
    void f(S);
}

void g() {
    N::S s;
    f(s);   // OK: calls N::f
    (f)(s); // error: N::f not considered; parentheses
            // prevent argument-dependent lookup
}

<ブロッククオート

-終了例 ]。

リストコンテキストでカンマ演算子を使用可能にする

カンマ演算子は、ほとんどのリストのようなコンテキスト(関数やテンプレートの引数、イニシャライザーリストなど)において特別な意味を持っています。形式の括弧 a, (b, c), d という形式の括弧を使うと、 通常の a, b, c, d という形式ではカンマ演算子が適用されません。

5.18 カンマ演算子[expr.comma]について

2 カンマに特別な意味が与えられている文脈では、[ 例. で 関数への引数のリスト (5.2.2) と初期化子のリスト (8.5) -例終了 ] 第 5 条で説明されているコンマ演算子は、括弧の中にのみ現れることができます。 は括弧の中にのみ出現します。[ 例

f(a, (t=3, t+2), c);

<ブロッククオート

は3つの引数を持ち、そのうちの2番目の引数は値5です。 ]

むつかしい構文の曖昧さ解消

C 言語とその難解な関数宣言構文との後方互換性は、vexing parses として知られる驚くべき解析のあいまいさにつながる可能性があります。本質的には 宣言としてパースできるものはすべて宣言としてパースされます。 として解析されます。

6.8 曖昧さ解消 [stmt.ambig] (英語)

1 式-ステートメントを含む文法には曖昧さがある と宣言 : 関数型の明示的な型変換(5.2.3)を左端の部分式とする式文は 明示的な型変換 (5.2.3) を左端の部分式として持つ式文は、最初の宣言子が開始される宣言と区別がつかないことがあります。 最初の宣言子が(.)で始まる宣言と見分けがつかない。 で始まる宣言と区別がつきません。 このような場合、ステートメントは宣言である .

8.2 曖昧さの解消 [dcl.ambig.res]

1 6.8で述べた関数型キャストと宣言の類似性から生じる曖昧さは、関数型キャストの文脈でも生じます。 と宣言の類似性から生じる曖昧さは、6.8で述べた 宣言の . この文脈では、パラメータを囲む冗長な括弧を持つ関数 パラメータを囲む冗長な括弧を持つ関数宣言と、関数スタイルのキャストを持つオブジェクト宣言のどちらを選ぶかです。 と、初期化子として関数型キャストを使ったオブジェクト宣言のどちらかを選択することになります。 のどちらかを選ぶことになります。6.8で述べた曖昧さと同じように、その解決策は の解決策は である可能性のあるすべての構成要素を 宣言とみなすことです。 . [注意: 宣言は、非関数型キャストや、宣言であることを示す = で明示的に区別することができます。 宣言は、非関数型キャスト、=による初期化、冗長な括弧の削除によって明確に区別されます。 による初期化、あるいはパラメータ名の周りの冗長な括弧を削除することで、宣言の曖昧さをなくすことができます。 パラメータ名の周りにある冗長な括弧を削除することです。-終了メモ ]。[ 例

struct S {
    S(int);
};

void foo(double a) {
    S w(int(a));  // function declaration
    S x(int());   // function declaration
    S y((int)a);  // object declaration
    S z = int(a); // object declaration
}

<ブロッククオート

-終了例 ]。

この例として有名なのが 最も厄介なパース の項目 6 で Scott Meyers によって広められた名前です。 効果的なSTL の本があります。

ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // warning! this doesn't do
               istream_iterator<int>());        // what you think it does

これは関数を宣言しています。 data であり、その戻り値の型は list<int> . この 関数dataは2つのパラメータをとります。

  • 最初のパラメータは dataFile . このパラメータは istream_iterator<int> . である。 を囲む括弧は dataFile は余計なので無視されます。
  • 第2パラメータは名前を持ちません。その型は、何も取らず を返す関数へのポインタです。 istream_iterator<int> .

最初の関数引数の周りに余分な括弧を置くと(2番目の引数の周りの括弧は違法です)、曖昧さが解消されます。

list<int> data((istream_iterator<int>(dataFile)), // note new parens
                istream_iterator<int>());          // around first argument
                                                  // to list's constructor

C++11 には brace-initializer 構文があり、多くのコンテキストでこのような解析の問題を回避することができます。

での参照性の推論 decltype の表現で参照可能である。

とは対照的に auto 型の推論を行います。 decltype は参照性(lvalueとrvalueの参照)を推論することを許可します。ルールでは decltype(e)decltype((e)) という表現があります。

7.1.6.2 単純な型指定子 [dcl.type.simple]

4 式の場合 e , で示される型は decltype(e) は次のように定義されます。 は次のように定義される。

- もし e が親展されていないid-expressionまたは 親展されていないクラスメンバーアクセス(5.2.5)である場合。 decltype(e) は型 によって名付けられたエンティティの e . そのような実体がない場合、あるいは e が がオーバーロードされた関数のセットを指定した場合、 プログラムは不正な形式となります。

- それ以外の場合は もし e がxvalueであれば decltype(e)T&& であり、ここで T のタイプは e ;

- それ以外の場合は e がl値である場合。 decltype(e)T& であり、ここで T は の e ;

- さもなければ decltype(e) の型は e .

のオペランドは評価されません。 decltype指定子のオペランドは評価されないオペランドです(条項5)。[ 例

const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = 0;   // type is const int&&
decltype(i) x2;           // type is int
decltype(a->x) x3;        // type is double
decltype((a->x)) x4 = x3; // type is const double&

<ブロッククオート

-終了例 ]。[注意:このルールでは decltype(auto) を含む型を決定するルールは,7.1.6.4に規定されている。]

のルールは decltype(auto) のルールは、初期化式のRHSにある余分な括弧に対しても同じような意味を持ちます。以下はその例で C++FAQ この関連する Q&A

decltype(auto) look_up_a_string_1() { auto str = lookup1(); return str; }  //A
decltype(auto) look_up_a_string_2() { auto str = lookup1(); return(str); } //B

1つ目の戻り値は string を返し、2つ目は string & への参照であり、これはローカル変数 str .

プリプロセッサマクロ関連エラーの防止

プリプロセッサ マクロには、C++ 言語との相互作用における多数の微妙な要素があります。

  • マクロ定義内でマクロ パラメータを括弧で囲む。 #define TIMES(A, B) (A) * (B); を使用することで、不要な演算子の優先順位を避けることができます (たとえば TIMES(1 + 2, 2 + 1) は 9 を生成しますが、括弧で囲まない場合は 6 を生成します。 (A)(B)
  • は、カンマを含むマクロ引数を括弧で囲んでいます。 assert((std::is_same<int, int>::value)); このようにしないとコンパイルできません。
  • 関数を括弧で囲むことで、インクルードされたヘッダでのマクロ展開から保護します。 (min)(a, b) (予期しない副作用として ADL も無効になります)