[C++]static constexprな配列メンバの定義

クラスの静的メンバ変数は通常クラス外にその定義が必要になります。

struct sample {
  //宣言
  static int n;
};

//定義
int sample::n = 10;

ただし、静的メンバ変数がstatic constexprな変数であるときは、多くの場合その定義は省略することができます(static constでもほぼ同様)。

struct sample {
  //宣言
  static constexpr int n = 10;
};

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

しかし、static constexprな配列メンバは同様の場合においても定義が必要とされます(ただし、C++14まで)。

struct sample {
  //宣言?
  static constexpr int m[] = {10, 20};
};

//定義、これが無いとリンカエラー(C++17以降は逆に非推奨)
constexpr int sample::m[];

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

これには変数がいつodr-usedされるのか、という難解な決まりごとが関わっています。

変数のodr-used

変数や関数などはodr-usedされたときにその定義が必要となります。逆に言えば、odr-usedされなければ定義は必要ではありません。

変数のodr-usedは以下のように規定されています(N4659 6.2.3 One-definition rule [basic.def.odr]より、C++17とC++14ではこの部分の内容に変更は無いのでC++17規格を参照します)。

A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion to x yields a constant expression that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion is applied to e, or e is a discarded-value expression.

細かく説明するのは困難なので簡単に要約すると、ある変数xは以下の場合を除いてodr-usedされます。

xが評価されうる式exに現われていて

  • xにlvalue-rvalue変換を適用すると、非トリビアルな関数を呼び出さない定数式、で行うことができる
  • xは参照である
  • xはオブジェクトであり、かつ
    • exはlvalue-rvalue変換が適用されない結果が廃棄される式(discarded-value expression)の(想定される)結果の一つであるか
    • exはlvalue-rvalue変換が適用可能な、より大きな式の(想定される)結果の一つ

まあ良く分からないですね・・・。
今回重要なのはなんとなくわかるかもしれない1つ目の条文

xにlvalue-rvalue変換を適用すると、非トリビアルな関数を呼び出さない定数式、で行うことができる

という所です。

lvalue-rvalue変換とはその名の通り、変数を左辺値から右辺値へ変換するものです。そして、それが非トリビアルな関数(ユーザ定義された関数)を呼び出さず、定数式で行える時、変数はodr-usedされません。

実は、非配列のstatic constexprな変数はほとんどの場合この規則に当てはまっており、使っている(つもり)のところではその定数値が直接埋め込まれる形に変換されているわけです。

struct sample {
  //宣言
  static constexpr int n = 10;
};

int main() {
  //このsample::nの使用は、lvalue-rvalue変換によって
  int n = sample::n;
  //以下のように書いたかのように扱われている
  int n = 10;
}

常にこのようにすることができる場所ではその定義は必要とならないので、odr-usedである必要がない事が分かるでしょう。

ところで、lvalue-rvalue変換とはなんぞやと詳しく見に行ってみると、1番最初に以下のように書かれています(N4659 7.1.1 Lvalue-to-rvalue conversion [conv.lval])。

A glvalue of a non-function, non-array type T can be converted to a prvalue.

要約すると

関数型でも配列型でもない型Tのglvalueは、同じ型のprvalueに変換できる。

おわかりいただけたでしょうか、static constexprな配列メンバを使用すると定義を必要とされるのはこの条文によります。
つまり、配列型の変数にはlvalue-rvalue変換を適用することができない(そして、odr-usedとならない他の条件にも当てはまらない)ため、どのように使ったとしてもodr-usedになってしまうのです。

結果、クラス外に定義が必要になってしまいます。

そして、例えばコンパイルをとりあえず通すためにヘッダにそのような定義を書き、長い時間が過ぎた後でそのヘッダを複数のソースファイル(翻訳単位)からインクルードしたとき、今度は定義が重複している、ODR違反だ!というエラーに悩まされることでしょう・・・

C++17以降の世界

C++17以降は、冒頭のstatic constexprな配列メンバにおいても定義をすることなく使用することができるようになります。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

これは、odr-usedの条件が変更されたわけではなく、C++17より導入されたinline変数の効果によるものです。

詳しくは → インライン変数 - cpprefjp C++日本語リファレンス

クラスの静的メンバ変数にconstexprがついているとき、暗黙的にinline指定したのと同じになり、定義がその場で生成されます(static constexprなメンバ変数については、むしろクラス外に定義を書くのは非推奨となります)。
しかも、inline変数なのでヘッダに書いて複数ファイルからインクルードしてもその定義は一つに保たれるためにODR違反に悩まされることもありません。

つまりはC++17以降を使いましょう、という事ですね・・・。

参考文献

謝辞

この記事は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース