c – clangとgccはテンプレート生成と静的なconstexprメンバを扱うとき異なる振る舞いをしますか?

次のプログラムを考えてみましょう(長さについては残念。これは私が問題を表現するために考えることができるこの最短の方法です):

#include <iostream>
#include <vector>
#include <typeindex>

using namespace std;

std::vector<std::type_index>&
test_vector()
{
  static std::vector<std::type_index> rv;
  return rv;
}

template <typename T>
class RegistrarWrapper;

template<typename T>
class Registrar
{
  Registrar()
  {
    auto& test_vect = test_vector();
    test_vect.push_back(std::type_index(typeid(T)));
  }
  friend class RegistrarWrapper<T>;
};

template <typename T>
class RegistrarWrapper
{
  public:
    static Registrar<T> registrar;
    typedef Registrar<T> registrar_t;
};

template <typename T>
Registrar<T> RegistrarWrapper<T>::registrar;


template <typename T>
class Foo
{
  public:
    // Refer to the static registrar somewhere to make the compiler
    // generate it ?!?!?!?
    static constexpr typename RegistrarWrapper<Foo<T>>::registrar_t& __reg_ptr =
      RegistrarWrapper<Foo<T>>::registrar;
};


int main(int argc, char** argv)
{
  Foo<int> a;
  Foo<bool> b;
  Foo<std::string> c;

  for(auto&& data : test_vector()) {
    std::cout << data.name() << std::endl;
  }

}

clang(バージョン3.5.2、もちろん-std = c 11)でコンパイルした場合、このプログラムは次のように出力します(読みやすくするためにc filtを介してパイプ処理します)。

Foo<int>
Foo<bool>
Foo<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >

しかしg(バージョン4.8.5、4.9.3、および5.2.0を試してみました)では、何も出力されません。何が起きてる?どのコンパイラがC標準に準拠していますか?どうすればこの効果をコンパイラに依存しない方法で(できればランタイムオーバーヘッドなしで)作成することができますか?

ベストアンサー
まず、2、3の解決策です。どちらの場合も、必須の部分は、インスタンス化が保証されているコードからレジストラのアドレスを取得することです。これにより、静的メンバーの定義も確実にインスタンス化され、副作用が引き起こされます。

1つ目は、Fooの各特殊化に対するデフォルトコンストラクタの定義が、メインのa、b、およびcのデフォルト初期化を処理するためにインスタンス化されるという事実に基づいています。

template<typename T> class Foo
{
public:
   Foo() { (void)&RegistrarWrapper<Foo<T>>::registrar; }
};

不利な点は、これが重要なコンストラクタを導入することです。この問題を回避する代替策は次のとおりです。

template<class T> constexpr std::size_t register_class() 
{ 
   (void)&RegistrarWrapper<T>::registrar; 
   return 1; 
}

template<typename T> class Foo
{
   static char reg[register_class<Foo<T>>()];
};

ここで重要なのは、初期化子に頼らずに静的メンバの宣言でインスタンス化を引き起こすことです(下記参照)。

どちらのソリューションもClang 3.7.0、GCC 5.2.0、およびVisual C 2015で最適化されています。最適化が有効でも有効でもありません。 2番目のものは、C 14機能であるconstexpr関数のための拡張された規則を使います。もちろん、必要に応じて、C 11準拠にする簡単な方法がいくつかあります。

あなたの解決策に伴う問題は、__ reg_ptrのための初期化子がその値がどこかで使われないならばインスタンス化されるという保証がないということだと思います。 N4527からのいくつかの標準的な引用:

14.7.1p2:

[…] the initialization (and any associated side-effects) of a static data
member does not occur unless the static data member is itself used in
a way that requires the definition of the static data member to exist.

これはconstexprのケースにはまったく対処していません。(私は思う)odr-usedの静的データメンバのクラス外定義について話しているからです(レジストラにとってより適切です)が、それは近いです。

14.7.1p1:

[…] The implicit instantiation of a class template specialization
causes the implicit instantiation of the declarations, but not of the
definitions, default arguments, or exception-specifications of the
class member functions, member classes, scoped member enumerations,
static data members and member templates […]

これにより、2番目の解決策が機能することが保証されます。静的データメンバのクラス内初期化子については何も保証されていないことに注意してください。

constexprコンストラクトのインスタンス化に関しては、不確実性があるようです。 CWG 1581がありますが、それは私たちの場合には関係ありませんが、最後に、constexprのインスタンス化が定数式の評価中に行われるのか、構文解析中に行われるのかが不明であるという事実について話します。この分野のいくつかの明確化はあなたの解決策にも同様にいくつかの保証を提供するかもしれません(どちらの方法でも…)、しかし我々は待つ必要があります。

3つ目の方法は、暗黙のインスタンス化に頼るのではなく、Fooの特殊化を明示的にインスタンス化することです。

template class Foo<int>;
template class Foo<bool>;
template class Foo<std::string>;

int main()
{
   for(auto&& data : test_vector()) {
      std::cout << data.name() << std::endl;
   }
}

これは3つのコンパイラすべてでも機能し、14.7.2p8に依存します。

An explicit instantiation that names a class template specialization
is also an explicit instantiation of the same kind (declaration or
definition) of each of its members […]

これらが明示的なインスタンス化定義であることを考えると、これはGCCに__reg_ptrの初期化子をインスタンス化するよう説得するのに十分なようです。ただし、これらの明示的なインスタンス化定義はプログラム全体で1回しか出現できないため([14.7p5.1])、特別な注意が必要です。私は最初の2つの解決策がより信頼できると考えています。

転載記事の出典を記入してください: c – clangとgccはテンプレート生成と静的なconstexprメンバを扱うとき異なる振る舞いをしますか? - コードログ