[C++]名前を必要としない変数のための変数名

C++26より、使用しない値に対する共通した変数名として_(U+005F、アンダースコア/アンダーバー)を言語サポート付きで使用できるようになります。

[[nodiscard]]
auto f() -> int;

auto g() -> std::tuple<int, double, std::string>;

int main() {
  auto _ = f(); // ok、警告なし
  auto [n, _, str] = g();   // ok

  std::cout << _;   // ng
}

概要

ローカル変数でその変数名が_であるものは、暗黙的に[[maybe_unused]]が指定されたように振る舞います。

[[nodiscard]]
auto f() -> int;

int main() {
  // この宣言は
  auto _ = f();
  
  // このように宣言されているのと同等
  [[maybe_unused]]
  auto _ = f();
}

これによって、この変数_は以降使用されていなくてもコンパイラは警告しません。また、[[nodiscard]]指定された関数では、戻り値を捨てている警告も抑制されます。

変数名_はまた、ローカルの構造化束縛の変数名としても使用でき、同じことが適用されます。

auto g() -> std::tuple<int, double, std::string>;

int main() {
  auto [_, d, str] = g(); // ok
  auto [n, _, _] = g();   // ok
  auto [_, _, _] = g();   // ok
}

構造化束縛の例からもわかるかもしれませんが、変数名_は同じスコープで何度でも使用することができます。

int main() {
  auto _ = 0;   // ok
  auto _ = f(); // ok
  auto [_, _, str] = g(); // ok
  double _ = 1.0; // ok
}

そして、_が2回以上宣言されている場合、そのコンテキストにおける_の利用はコンパイルエラーとなります。

void h(auto);

int main() {
  auto _ = 0; // ok

  std::cout << _ << '\n'; // ok、この時点では使用可能

  auto _ = 1.0; // ok

  std::cout << _ << '\n'; // ng
  _ = f();    // ng
  int n = _;  // ng
  h(_);       // ng
}

この場合、_は変数名として宣言することにのみ使用できます。

このように、C++26における変数名_は初期化後使用しないために名前を必要としない変数に対する共通の名前として使用できるようになります。

そして、この機能はまた、将来のパターンマッチング構文で_を使用するための前準備でもあります。

name-independent declaration

ここでは細かい規定の話を掘り下げます。興味がなければ読み飛ばしても大丈夫です。

自動記憶域期間(automatic storage duration)を持つ変数の宣言で、その名前が_であるものは 名前に依存しない宣言(name-independent declaration として扱われます。また同様に、その名前が_である次の宣言も名前に依存しない宣言となります

  • 名前空間スコープ以外の構造化束縛
  • 初期化キャプチャ
  • staticメンバ変数

名前に依存しない宣言に対する実装への推奨事項として、実装は名前に依存しない宣言(の変数名)が使用されていること、あるいは使用されていないことに関する警告を発しない、ことが推奨されています。これは強制ではなく推奨事項なので、もしかしたらこれに従わずに_変数の未使用について警告するコンパイラが存在する可能性はあります。この記事執筆時点でこの機能を実装しているのはClangのみですが、Clangはこの推奨にしたがっています。

そして、名前に依存しない宣言は後の宣言で同じ名前によって宣言されている時でも、名前が衝突しているとはみなされません。現在、名前に依存しない宣言で許可されている名前は_のみなので、_を使用した名前に依存しない宣言は何度でも再宣言可能となります。

[[nodiscard]]
constexpr auto f() -> int;

// 名前空間スコープでは、名前に依存しない宣言は考慮されない
auto _ = f(); // ok、名前に依存しない宣言ではない
auto _ = f(); // ng、同名変数の再宣言

// 上記宣言がなかったとしても
auto [_, _] = std::make_pair(10, 1.0);  // ng、名前が重複している


// メンバ変数
struct S {
  int _ = 0;    // ok、名前に依存しない宣言
  int _ = f();  // ok、名前に依存しない宣言

  auto _ = f(); // ng、メンバ変数宣言ではautoを使用不可

  static int _ = 0; // ng、同名変数の再宣言
};

int main() {
  // グローバルの_を隠蔽する
  int _ = 0;    // ok、名前に依存しない宣言
  auto _ = f(); // ok、名前に依存しない宣言

  // 構造化束縛
  auto [_, _] = std::make_pair(10, 1.0);  // ok、名前に依存しない宣言

  // ラムダ式の初期化キャプチャ
  auto _ = [_ = 0, _ = f()](auto) {}; // ok、キャプチャはどちらも名前に依存しない宣言

  static int _ = 0; // ng、同名変数の再宣言
}

ただし、名前に依存しない宣言の衝突が許容されるのは再宣言のみであり、その名前(_)が2つ以上の異なるエンティティ(変数宣言)に対応する(つまり_が2回以上宣言されている)場合、_の名前解決は失敗しコンパイルエラーとなります。

[[nodiscard]]
constexpr auto f() -> int;

int main() {
  int _;  // ok、名前に依存しない宣言

  // _の名前解決先は1つに定まる
  _ = 0;      // ok
  int n = _;  // ok

  auto _ = f(); // ok、名前に依存しない宣言
  
  // 名前候補に2つの宣言が該当し、名前解決に失敗する
  ++_;  // ng
}

なお、名前に依存しない宣言は宣言のコンテキストとその名前が_であるかによって決まるため、型の指定は自由であり必ずしもautoである必要はありません。ただし、クラススコープではauto変数宣言は使用できません。

上記の4種類に該当しない宣言は_をその名前に使用していても名前に依存しない宣言ではないため、これらの特別扱いはされません。それは上記例のように名前空間スコープの変数やstatic変数などが該当しますが、それ以外のもので名前空間スコープに無いながらも該当しないものとして、関数引数とNTTPがあります。

void f(int _,     // ok、_という名前の仮引数宣言(名前に依存しない宣言ではない)
       double _   // ng、仮引数名が衝突している
      );

template<auto _,  // ok、_という名前のNTTPの宣言(名前に依存しない宣言ではない)
         int _    // ng、NTTP名が衝突している
        >
struct S{};

これらの名前については、それを使用しない場合はその意図として省略する(無名にする)ことができます。関数引数の場合はマクロの使用や関数テンプレート内の分岐などによって使用するかどうかが変化する場合がありますがその場合でもそのために_は使えないため、仮引数宣言に[[maybe_unused]]を指定することで警告を抑えるしかありません。

名前空間スコープの_名はそれが変数名であっても名前に依存しない宣言ではないため、その名前をusing宣言によってローカルスコープに導入する場合は、そのコンテキストで名前に依存しない宣言が存在していてはなりません。逆に、そのコンテキストでグローバルの_usingされている場合でも、名前に依存しない宣言を使用することができます。

int _;  // ok、グローバル変数

void f() {
  int _;  // ok、名前に依存しない宣言(B)
  _ = 0;  // ok

  using ::_; // ng、このusing宣言はBより後に来られない
}

void g() {
  using ::_; // ok、名前に依存しない宣言よりも前にあれば良い
  _ = 0;  // ok、::_を更新

  int _ = 10; // ok、名前に依存しない宣言

  _ = -1; // ng
}

破棄のタイミング

C#やRustなどの_変数名と異なる点として、C++26の_変数名はその値の破棄(デストラクタ呼び出し)を意味していません。あくまで、その変数(オブジェクト)の利用に興味がないことを意味しています。

したがって、_変数の破棄のタイミングは他の変数と同じになり、そのスコープの終わりで宣言と逆順に破棄されます。

struct check_dtor {
  int n;

  check_dtor(int a) : n(a) {}

  ~check_dtor() {
    std::println("Destructor called {:d}.", n);
  }
};

int main() {
  auto _ = check_dtor{1};
  auto _ = check_dtor{2};

  {
    auto _ = check_dtor{3};
  }

  auto _ = check_dtor{4};
}
Destructor called 3.
Destructor called 4.
Destructor called 2.
Destructor called 1.

デストラクタがトリビアルな型の場合は最適化によってスタック上から消し去られる可能性はありますが、C++の意味論としては_変数に束縛されたオブジェクトが即座に破棄されることはありません。

これはまた、[[nodiscard]]な戻り値を破棄するテクニックとの違いでもあります。

[[nodiscard]]
auto f() -> int;

int main() {
  // いずれも警告されない
  auto _ = f();       // 戻り値は破棄されていない
  (void)f();          // 戻り値はこの行で破棄される
  std::ignore = f();  // 戻り値はこの行で破棄される
}

このようになっているのは、RAII以外の役割を持たない型のオブジェクトに対する変数名として_を使用できるようにすることを意図しているためです。

std::mutex mtx{};

auto f() {
  // lock_guardのオブジェクトはRAIIのためだけに必要
  std::lock_guard _{mtx};

  ...
}

auto g() {
  using namespace std::experimental;

  // scope_exitのオブジェクトもRAIIのためだけに必要
  scope_exit _{[]() { ... }};

  ...
}

std::lock_guard等によるstd::mutexのロックが分かりやすいと思いますが、これらのオブジェクトはRAIIのためだけに宣言が必要であり、初期化後にアクセスする必要が全くありません。従来はこのような変数に対しても名前を付ける必要があり、そのようなものが同じスコープに複数ある場合は特に命名に少し面倒さがありました。そこに_を使用することで、このようなRAIIオブジェクトがいくつあったとしても1つの共通の意味を持つ名前を使用できるようになります。

このようなRAIIオブジェクトの変数名のために_を使用するようにする場合、_の初期化の行でそのオブジェクトが即破棄されると意図通りになりません。そのため、_変数名で初期化されているときでも、そのオブジェクト実体の破棄順序は通常と同じになっています。

後方互換について

C++23以前から_そのものはソース文字集合の要素の1つであり、任意のC++エンティティの名前(変数名や関数名など)として使用することができました。ただし、_自身も含めた_から始まるグローバルスコープの任意の名前は標準によって予約されており、その使用は未定義動作となります(多くのコンパイラは別にそれについて警告を発しないようですが)。したがって、_プログラマが自由に使用できていたのはローカルスコープのみとなります。

そのため、_変数名が一度しか宣言されていない場合は従来意図して_を使用していたコードと区別ができません。一方で、変数名として使用した時にはあるスコープに一度しかその名前は宣言できません。そのため、従来のコードの動作を変えないために_が一度だけ宣言されている場合は通常の変数名とほぼ変わりなく扱われており、複数回宣言された場合にのみその使用がコンパイルエラーとなるようになっています。

// C++23以前のコードとする

double _ = 1.0; // UB、予約語

int main() {
  int _ = 0;  // ok

  // 再代入や値の読み取りもできる
  _ = 10;     // ok
  int n = _;  // ok

  // 他の変数同様に再宣言できない
  int _ = 20; // ng、C++26からok
}

規定における名前に依存しない宣言への推奨事項である「実装は名前に依存しない宣言(の変数名)が使用されていること、あるいは使用されていないことに関する警告を発しない」というのの使用されていることについての警告も発しないという指定は、_変数名が一度しか宣言されていない場合の使用を念頭に置いたものです。

それでも一応、この機能の導入に当たって、_という名前が実際に使用されているのかどうかが調査されたところ、2つの例が見つかりました。まず1つはGoogle Mockというライブラリで、グローバル変数名として使用されていました。

// Google Mockにおける_使用例
namespace testing {
  
  const internal::AnythingMatcher _ = {};

}

おそらくこのような使用例との衝突を回避するために、C++26の_変数名の特別扱いはローカルスコープ(関数スコープ)とクラススコープのみに制限されており、_変数名は必ず何かしらの変数の宣言として導入されます。そのため、他の名前に使用されていてもその名前を隠蔽するだけで、この利用例に対して過度に影響を与えることはないはずです。

2つ目は、Gettextというライブラリを使用するプロジェクトにおいてのもので、そこでは_はGettextのラッパの関数マクロとしてよく使用されています。

#define _(msgid) gettext (msgid)

ただしこれは、Gettextライブラリヘッダ自体が提供するものではなく、あくまでその利用側での慣習として良く定義されているものです。また、この場合は関数マクロであるので、この機能の_とはあまり衝突しません。

constexpr const char* gettext(int) { return nullptr;}
#define _(msgid) gettext (msgid)

int main() {
  constexpr auto _ = _(42); // ok
  auto _ = 42;  // ok
  static_assert(_ == nullptr);  // ng
}

この2つの名前空間スコープにおける例はいずれも厳密に言えば未定義動作にあたっていますが。C++26のこの機能はどちらに対する影響も抑えたものになっています。

参考文献

この記事のMarkdownソース