LoginSignup
31
16

More than 3 years have passed since last update.

クラスメンバに constexpr static 変数はおすすめしない ― 現象と対策

Last updated at Posted at 2019-07-12

この記事について

クラスの静的メンバ変数に constexpr があると困ることがあるよ、という話と、じゃあどうすればいいかという話。

実際に困るパターン

まあまずはソースコード。

c++11
// -std=c++11 -Wall
#include <iostream>

template <typename type> void foo(type const &v) {
  std::cout << v << std::endl;
}

template <typename type>
struct bar
{
  constexpr static type value = 123;
};

int main() {
  foo(bar<int>::value); // Undefined symbols(略) "bar::value"
  return 0;
}

これがエラーになる。
リンクエラー。
エラーになるのは、bar<int>::value定義がないから。

bar は template なので、bar<int>::value の定義なんて書いてられない。困る。

クラスのメンバではない

c++11
constexpr static int out_of_class_constant = 123;

は、宣言が定義を兼ねるのでエラーにならない(対比のために static と書いたが、 static は不要)が、クラススコープにある

c++11
  constexpr static type value = 123;

は、宣言なので、別途定義が必要になるかもしれない

「かもしれない」と書いたのは。
コンパイラが「値がわかればいいや」と思っていれば定義がなくても困らないのでコンパイル→リンクできる。コンパイラが「アドレスほしいなぁ」と思ったら定義を探しに行き、定義がないとリンクエラーになる。つまり、コンパイラの気持ち次第。

上記のソースコードの場合、const参照を引数としている関数に渡しているだけなのでアドレスなんて必要そうにないけど、なぜだか g++-9clang もアドレスを取りに行くらしく、エラーになる。

コンパイラ様のお気持ちなので、ちょっとした変更でエラーじゃなくなる。
実際、g++-9 ではコンパイラオプション -O3 をつけるだけでビルドが通ったりする(clang++ では依然として通らないんだけど)。

邪悪な対策

アドレスを取りに行くのが原因なので、上記のコードの main関数を以下のように変えるとビルドが通る。

c++11
int main() {
  foo(bar<int>::value + 0); // 無駄に0を足すとアドレスを取らなくなる
  return 0;
}

偶然なのか必然なのかはよくわからないが、手元のコンパイラではそうなる。

とはいえ、こんな対策いやだよね。

邪悪じゃない対策(とりあえず)

値のアドレスを取られなければいいので、関数にすれば良い。
こんな感じ。

c++11
// -std=c++11 -Wall
#include <iostream>

template <typename type> void foo(type const &v) {
  std::cout << v << std::endl;
}

template <typename type> struct bar {
  constexpr static type value() { return 123; }
};

int main() {
  foo(bar<int>::value());
  return 0;
}

整数なら enum にするという手もあるけど、そうできないこともある。
関数になんかしたくないんだけど、関数にするしか手がない。

邪悪じゃない対策(C++17)

と思っていたら新しい言語機能が。

c++17
// -std=c++17 -Wall
#include <iostream>

template <typename type> void foo(type const &v) {
  std::cout << v << std::endl;
}

template <typename type>
struct bar
{
  inline constexpr static type value = 123; // inline 変数
};

int main() {
  foo(bar<int>::value);
  return 0;
}

という具合。
inline 変数にすると、クラスの外にある変数と同じように振る舞う。
なお。 inline 変数が使える C++17 以降では

constexpr な静的データメンバは暗黙に inline

というスペシャルルールが加わったので、上記のソースに書いた inline は不要。C++17 としてコンパイルするだけでよい。

というわけで

というわけで、C++17 以降で仕事したいね。

環境

動作確認に使った環境は以下の通り:

OS: macOS Mojave 10.14.5
clang: Apple LLVM version 10.0.1 (clang-1001.0.46.4)
gcc: g++-9 (Homebrew GCC 9.1.0) 9.1.0

31
16
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
16