😲

C++ の無名共用体の仕様に驚いた話

2022/04/10に公開

はじめに

C++ には anonymous union という機能があります。
JIS 規格は参照していませんが、日本語では「無名共用体[1]」と呼ぶのが一般的なようです。 本記事でもこの呼び方を採用します。
どうも C++98 から存在していた機能のようですが、私はこれの存在を知らなかったので実際に遭遇して驚きました。

なお、本記事における C++ は C++20 を想定しており、規格書は N4861 を参照しています。

遭遇したコード

思いっきり抜粋しましたが、次の Hoge クラスのプライベートメンバが無名共用体です。

class Hoge {
 public:
  class A { ... };
  class B { ... };

 private:
  // これが無名共用体
  union {
    A a_;
    B b_;
  };
};

ご存知の方には何でもないコードでしょうが、私は理解できなくて (失礼ながら) 次のどちらかではないかと想像しました:

  • この無名共用体型のデータメンバの宣言を忘れている。
  • 共用体に型名をつけるのを忘れている。

しかし、驚いたことにこれで正解なのです。

無名共用体

無名共用体とは

規格 ([class.union.anon]) によると、

  • union { member-specification } ; の形の共用体を無名共用体と呼び、これは無名の型とその型の無名のオブジェクトを定義する。
  • 無名共用体の定義以降は、そのメンバは無名共用体が宣言されたスコープに定義されているとみなされる。

1 番目ですでに驚きなのですが、無名の型が定義されるだけでなく、その型の無名のオブジェクトも同時に定義されるということです。
無名のオブジェクトだとアクセスできなくて使いものにならないのではないかと考えてしまいますが、 2 番目にあるように、無名共用体のメンバはオブジェクト名がなくてもアクセスできるのです。 こちらも驚きです。

規格の例が簡潔でわかりやすいと思います:

Example 1 ([class.union.anon])
  void f() {
    union { int a; const char* p; };
    a = 1;
    p = "Jennifer";
  }

ここでは a に整数 1 を, p に文字列 "Jennifer" (のアドレス) を, それぞれ代入していますが、これら ap は関数 f のローカル変数ではなく、無名共用体の無名のオブジェクトのメンバです。
共用体なので ap は同一のアドレスに配置されます。
つまり、次の Example 2 のコードと同じ結果となります:

Example 2
  void f() {
    union { int a; const char* p; } u;
    u.a = 1;
    u.p = "Jennifer";
  }

そのため、無名共用体はシンタックスシュガーのひとつだと私は解釈しました[2]

なお、ちょっとわかりにくいのですが、 Example 2 のように共用体の定義に変数宣言をともなうものは無名共用体とはみなされません。 そのため、代入時に u. を省略するとエラーとなります。

用途

Example 1 のような使い方はありがたみがわからないですね。 Example 2 に比べてそれほどメリットがあるとは思えず、むしろわかりにくいかもしれないです。 (もちろん、これは説明のための例なのでそれでよいのです)
一方、冒頭の「遭遇したコード」のように他のクラスのメンバとして無名共用体を使うのはメリットがありそうです。 そのメンバが public の場合は特に。
なお、そのようなクラスに規格では "union-like class" と専用の用語を与えています。

そのほかの主な特徴や制約

  • 無名共用体のメンバ宣言は、非静的データメンバの定義か static_assert 宣言のどちらか。
  • 無名共用体内では、ネストされた型, 無名共用体, メンバ関数は宣言できない。
  • 無名共用体のメンバの名前は、その無名共用体が宣言されたスコープ内のあらゆるエンティティの名前と区別できなくてはならない。
  • 名前つき名前空間かグローバル名前空間内で宣言された無名共用体は static で宣言されなくてはならない。
  • 無名共用体は private および protected のメンバを持てない。
  • 無名共用体はメンバ関数を持てない。

おわりに

struct, class, enum でも名前を指定せずに無名の型を定義することはできますが、無名共用体のように型と同時に無名のオブジェクトが定義されたり、外からメンバが見えるようになったりはしません (unscoped enumeration type を除く)。 C++ としてはめずらしく、他の構文と一貫性がないと感じました。

今回の件で、標準ライブラリではなくコア言語にも、存在すら知らない機能がまだまだあるということを実感しました。

脚注
  1. 規格では "anonymous" という形容詞は、 "anonymous parameter" ([cmp.categories.pre]) という唯一の例外を除くと anonymous union のみに使われています。 これ以外の「無名」は "unnamed" が使われている印象です。 この使い分けを尊重すると「匿名共用体」などと呼ぶのがよさそうですが... ↩︎

  2. そのような規格中の記述はありませんが。 ↩︎

Discussion