C++は非常に強力で柔軟なプログラミング言語ですが、その複雑さの一因となっているのが豊富な演算子の存在です。

数式のような単純な計算から、ポインタ操作、ビット演算、そしてC++20以降で導入された三方向比較演算子(宇宙船演算子)にいたるまで、多種多様な演算子が定義されています。

これらの演算子を組み合わせて複雑な式を記述する際、どの操作が優先的に実行されるかを正しく理解しておくことは、バグを未然に防ぎ、可読性の高いコードを記述するために不可欠です。

本記事では、最新のC++標準に基づいた演算子の優先順位と結合規則を網羅的に解説し、開発者が陥りやすい落とし穴についても詳しく掘り下げていきます。

演算子の優先順位と結合規則の基本概念

C++の式の中に複数の演算子が含まれている場合、コンパイラは一定のルールに従って計算の順序を決定します。

このルールを構成するのが「優先順位」「結合規則」の2つの概念です。

優先順位(Precedence)とは

優先順位とは、異なる種類の演算子が並んでいるときに、どの演算子を先に評価するかを定めた順序です。

例えば、数学のルールと同様に、C++でも加算(+)よりも乗算(*)の方が優先順位が高く設定されています。

int result = 5 + 3 * 2; という式では、まず 3 * 2 が計算され、その後に 5 が足されるため、結果は 11 となります。

もし優先順位が同じであれば、次に説明する「結合規則」によって順序が決まります。

結合規則(Associativity)とは

結合規則とは、同じ優先順位を持つ演算子が連続している場合に、左から右へ計算するか、右から左へ計算するかを決めるルールです。

  • 左結合(Left-to-Right): ほとんどの演算子(算術演算子、論理演算子など)がこれに該当します。例えば a - b - c(a - b) - c と解釈されます。
  • 右結合(Right-to-Left): 代入演算子や単項演算子がこれに該当します。例えば a = b = ca = (b = c) と解釈され、まず c の値が b に代入され、その結果が a に代入されます。

C++ 演算子の優先順位・結合規則一覧表

以下に、C++における演算子の優先順位を、優先度の高い順(レベル1)から低い順(レベル17)にまとめました。

同じレベルに属する演算子は、同じ優先順位を持ちます。

優先順位演算子説明結合規則
1::スコープ解決左結合
2() [] . -> ++ --関数呼び出し、配列添字、メンバアクセス、後置増分・減分左結合
3++ -- + - ! ~ * & sizeof new delete前置増分・減分、単項プラス・マイナス、論理否定、ビット反転、間接参照、アドレス、サイズ、動的メモリ確保・解放右結合
4.* ->*メンバポインタアクセス左結合
5* / %乗算、除算、剰余左結合
6+ -加算、減算左結合
7<< >>ビットシフト(左、右)左結合
8<=>三方向比較(宇宙船演算子 / C++20以降)左結合
9< <= > >=比較(未満、以下、より大きい、以上)左結合
10== !=等価、不等価左結合
11&ビット論理積(AND)左結合
12^ビット排他的論理和(XOR)左結合
13|ビット論理和(OR)左結合
14&&論理積(AND)左結合
15||論理和(OR)左結合
16?: = op= throw条件演算子(三項演算子)、代入、複合代入、例外送出右結合
17,コンマ(順次評価)左結合

間違いやすい演算子の優先順位と注意点

一覧表を確認すると分かる通り、C++の優先順位は直感に反する場合がいくつかあります。

特にバグの原因になりやすいポイントを詳しく解説します。

1. ビット演算と等価演算の優先順位

最も注意が必要なのが、ビット演算子(&, ^, |)と等価演算子(==, !=)の関係です。

多くのエンジニアは「ビット演算でフラグを抽出してから、その結果をチェックする」という処理を書きますが、優先順位は == の方が & よりも高くなっています。

意図しない挙動の例: if (flags & MASK == 0x01)

このコードは、開発者の意図((flags & MASK) == 0x01)に反して、flags & (MASK == 0x01) として評価されてしまいます。

ビット演算を行う際は、必ず ( ) で囲む癖をつけましょう。

2. ポインタ操作とインクリメント

ポインタの参照(*)と後置インクリメント(++)の組み合わせも、C++初学者が混乱しやすいポイントです。

  • ptr++ : これは (ptr++) と同等です。つまり「ポインタが指す値を取得した後、ポインタ自体を次の要素に進める」という動作になります。
  • (*ptr)++ : これは「ポインタが指している場所にある数値そのものをインクリメントする」という動作になります。

後置インクリメント(レベル2)は間接参照(レベル3)よりも優先順位が高いため、このような挙動になります。

3. 論理積(&&)と論理和(||)の優先順位

論理演算において、&&|| よりも優先順位が高いです。

これは数学における「掛け算(AND)は足し算(OR)より先」という概念に基づいています。

if (a || b && c)

この場合、まず b && c が評価され、その結果と a が OR 演算されます。

条件式が複雑になる場合は、優先順位に関わらず括弧を使用して、評価の意図を明示することを強く推奨します。

4. シフト演算子と算術演算子

ビットシフト演算子(<<, >>)は、加算・減算(+, -)よりも優先順位が低いです。

int n = 1 << 2 + 1;

このコードの結果は、(1 << 2) + 15 になるのではなく、1 << (2 + 1)8 になります。

シフト演算を用いて倍数計算を行う場合などは注意が必要です。

実践プログラムによる優先順位の確認

実際に、優先順位の違いがプログラムの結果にどのような影響を与えるかを確認してみましょう。

以下のコードでは、括弧の有無によって評価順序が変わり、出力結果が変化する様子を示しています。

C++
#include <iomanip>
#include <iostream>

int main() {
    // ケース1: ビット演算と等価演算の優先順位
    unsigned int flags = 0x05; // 0101
    unsigned int mask = 0x04;  // 0100

    // 括弧なし: flags & (mask == 0x04) と評価される
    // mask == 0x04 は true (1) なので、 0x05 & 1 = 1 となる
    bool result1 = flags & mask == 0x04;

    // 括弧あり: (flags & mask) == 0x04 と評価される
    // 0x05 & 0x04 は 0x04 なので、 0x04 == 0x04 = true (1) となる
    bool result2 = (flags & mask) == 0x04;

    std::cout << "--- ケース 1: ビット演算 AND と 等価演算 ---" << std::endl;
    std::cout << "括弧なしの結果: " << result1
              << " (1を期待した? いいえ、評価順序が異なります)" << std::endl;
    std::cout << "括弧ありの結果: " << result2 << std::endl;

    // ケース2: ポインタとインクリメント
    int arr[] = {10, 20, 30};
    int* p = arr;

    std::cout << "\n--- ケース 2: ポインタとインクリメント ---" << std::endl;
    int val = *p++; // pが指す値(10)を取得した後、pを次に進める
    std::cout << "*p++ の値: " << val << std::endl;
    std::cout << "インクリメント後の指し示す値: " << *p << std::endl;

    // ケース3: 三方向比較演算子 (C++20以降)
    // 宇宙船演算子は比較演算子より優先順位が高い
    int a = 10, b = 20;
    auto res = a <=> b; // 戻り値は std::strong_ordering

    std::cout << "\n--- ケース 3: 三方向比較演算子 (宇宙船演算子) ---"
              << std::endl;
    if (res < 0) {
        std::cout << "a は b より小さい" << std::endl;
    } else if (res > 0) {
        std::cout << "a は b より大きい" << std::endl;
    } else {
        std::cout << "a と b は等しい" << std::endl;
    }

    return 0;
}
実行結果
--- ケース 1: ビット演算 AND と 等価演算 ---
括弧なしの結果: 1 (1を期待した? いいえ、評価順序が異なります)
括弧ありの結果: 1

--- ケース 2: ポインタとインクリメント ---
*p++ の値: 10
インクリメント後の指し示す値: 20

--- ケース 3: 三方向比較演算子 (宇宙船演算子) ---
a は b より小さい

※ケース1において、たまたま結果が両方 1(true)になっていますが、内部的な評価プロセスは全く異なります。

たとえば mask0x02 だった場合、結果は劇的に変わります。

このように「たまたま動く」コードは潜在的なバグの温床となります。

結合規則が重要になるケース

優先順位が同じ演算子が並んだ場合、結合規則が式の意味を決定します。

特に重要なのが代入演算子の「右結合」と、コンマ演算子の「左結合」です。

代入演算子の連鎖(右から左)

C++では、複数の変数に同じ値を一度に代入することができます。

x = y = z = 100;

この式は、右結合により x = (y = (z = 100)) と解釈されます。

まず z100 が代入され、その代入式の評価結果(つまり 100)が順次左の変数へ引き渡されます。

条件演算子(三項演算子)のネスト

条件演算子 ?: も右結合です。

a ? b : c ? d : e

これは a ? b : (c ? d : e) と解釈されます。

ネストされた条件分岐を記述する際には、このルールを把握していないと意図しない分岐へ進む可能性があります。

ただし、可読性の観点からは、三項演算子をネストするよりも if-else 文を使うべきでしょう。

短絡評価(ショートサーキット)の仕組み

演算子の優先順位とは別に、論理演算子(&&||)には短絡評価(Short-circuit evaluation)という重要な特性があります。

  • && (論理積): 左辺が false の場合、右辺は評価されません(全体が必ず false になるため)。
  • || (論理和): 左辺が true の場合、右辺は評価されません(全体が必ず true になるため)。

これを利用して、if (ptr != nullptr && ptr->value > 0) のように、ヌルポインタチェックの後に安全にメンバへアクセスする手法が一般的に使われます。

この場合、優先順位以上に「評価の順序」がプログラムの安全性を担保しています。

優先順位に迷ったときのベストプラクティス

演算子の優先順位を完璧に暗記することは素晴らしいことですが、実務においては以下の3つのルールを意識することが、チーム開発や保守性の向上に繋がります。

1. 括弧 () を積極的に活用する

優先順位が明確であっても、あえて括弧を使うことで、コードを読む人に対して「この順序で評価されることを意図している」というメッセージを伝えることができます。

if ((a & MASK) == EXPECTED) と書くことで、一目でビット比較であることが分かります。

2. 複雑な式を分割する

一つの式にあまりに多くの演算子を詰め込むと、優先順位に関わらず理解しにくいコードになります。

auto intermediate = some_func(a) + b * c; auto final_result = intermediate << 2; のように、中間変数を利用して式を分けることで、デバッグも容易になります。

3. C++20の宇宙船演算子を活用する

比較演算が複雑になる場合、C++20で導入された <=>(三方向比較演算子)を使うことで、一貫性のある比較処理を簡潔に記述できるようになりました。

優先順位のレベル8に位置することを意識しつつ、新しい標準機能を積極的に取り入れましょう。

まとめ

C++の演算子優先順位と結合規則は、言語仕様の根幹をなす重要なルールです。

算術演算のような直感的なものから、ビット演算と等価演算の優先順位の逆転、ポインタと増分演算子の組み合わせなど、注意が必要なポイントが数多く存在します。

本記事で紹介した一覧表をリファレンスとして活用しつつ、少しでも優先順位に迷いや不安がある場合は、括弧を使用して意図を明確にすることを徹底してください。

正確な知識に基づいたコーディングは、バグの削減だけでなく、将来の自分や他の開発者がコードを読み解く際の手助けとなります。

C++の豊富な演算子を正しく使いこなし、堅牢で美しいプログラムを構築していきましょう。