4

偶然見つけた奇妙な例を皆さんと共有したいと思い、それで 2 日間考え続けました。

この例が機能するには、次のものが必要です。

  • 三角形の仮想継承 (メンバー関数上getAsString())
  • テンプレート クラスのメンバー関数の特殊化 (ここではValue<bool>::getAsString()) 仮想関数をオーバーライドする
  • コンパイラによる (自動) インライン化

共通のインターフェース、つまり一連の仮想関数を仮想的に継承するテンプレート クラスから始めます。後で、これらの仮想機能の 1 つを特殊化します。インライン化すると、スペシャライゼーションが見過ごされる可能性があります。

// test1.cpp and test2.cpp
#include <string>

class ValueInterface_common
{
public:
  virtual ~ValueInterface_common() {}
  virtual const std::string getAsString() const=0;
};

template <class T>
class Value :
  virtual public ValueInterface_common
{
public:
  virtual ~Value() {}
  const std::string getAsString() const;
};

template <class T>
inline const std::string Value<T>::getAsString() const
{
  return std::string("other type");
}   

次に、このクラスと、それ自体もテンプレート化する必要がValueあるクラスのインターフェイスを継承する必要があります。Parameter

// test1.cpp
template <class T>
class Parameter :
  virtual public Value<T>,
  virtual public ValueInterface_common
{
public:
  virtual ~Parameter() {}
  const std::string getAsString() const;
};

template<typename T>
inline const std::string Parameter<T>::getAsString() const
{
  return Value<T>::getAsString();
}

Value今、 bool と等しい型の特殊化の前方宣言を与えないでください (!) 。

// NOT in: test1.cpp
template <>
const std::string Value<bool>::getAsString() const;

しかし、代わりに、このように定義するだけです...

// test2.cpp
template <>
const std::string Value<bool>::getAsString() const
{
  return std::string("bool");
}

..しかし、別のモジュールで(それは重要です)!

最後に、main()何が起こっているかをテストする関数があります。

// test1.cpp
#include <iostream>

int main(int argc, char **argv)
{
  ValueInterface_common *paraminterface = new Parameter<bool>();
  Parameter<int> paramint;
  Value<int> valint;
  Value<bool> valbool;
  Parameter<bool> parambool;

  std::cout << "paramint is " << paramint.getAsString() << std::endl;
  std::cout << "parambool is " << parambool.getAsString() << std::endl;
  std::cout << "valint is " << valint.getAsString() << std::endl;
  std::cout << "valbool is " << valbool.getAsString() << std::endl;
  std::cout << "parambool as PI is " << paraminterface->getAsString() << std::endl;

  delete paraminterface;

  return 0;
}

次のようにコードをコンパイルすると (test1.cpp と test2.cpp という名前の 2 つのモジュールにコードを配置し、後者には特殊化と必要な宣言のみが含まれます):

g++ -O3 -g test1.cpp test2.cpp -o test && ./test

出力は

paramint is other type
parambool is other type
valint is other type
valbool is bool
parambool as PI is other type

-O0またはだけでコンパイルした場合、または-fno-inline特殊化の前方宣言を行った場合、結果は次のようになります。

paramint is other type
parambool is bool
valint is other type
valbool is bool
parambool as PI is bool

おかしいですね。

これまでの私の説明は次のとおりです。インライン化は最初のモジュール (test.cpp) で機能しています。必要なテンプレート関数はインスタンス化されますが、一部は への呼び出しでインライン化されてしまいますParameter<bool>::getAsString()。一方、valboolこれは機能しませんでしたが、テンプレートはインスタンス化され、関数として使用されます。次に、リンカーは、インスタンス化されたテンプレート関数と、2 番目のモジュールで指定された特殊化されたテンプレート関数の両方を見つけ、後者を決定します。

どう思いますか?

  • この動作はバグだと思いますか?
  • どちらも仮想関数をオーバーライドしているのに、インライン化Parameter<bool>::getAsString()が機能するのに機能しないのはなぜですか?Value<bool>::getAsString()
4

1 に答える 1

4

ODR の問題があると推測するので、一部のコンパイラーの最適化が別のコンパイラー設定と異なる動作をする理由を推測してもほとんど意味がありません。

本質的に、One Definition Ruleは、同じエンティティがアプリケーション全体でまったく同じ定義を持つべきであると述べています。そうでなければ、影響は未定義です。

根本的な問題は、クラス テンプレート メンバー関数の特殊化されたバージョンが表示されないコードがコンパイルされ、リンクされる可能性があり、場合によっては実行される可能性があることです。これは、明示的な特殊化 (の前方宣言) がない場合、特殊化されていないバージョンが開始され、特殊化された型でも機能する汎用機能が実装される可能性が高いためです。

したがって、運が良ければ、宣言/定義が欠落しているというコンパイラ エラーが発生しますが、本当に運が悪いと、意図したとおりに動作しない「機能する」コードが得られます。

修正: すべてのテンプレートの特殊化の (前方) 宣言を常に含めます。それらを単一のヘッダーに入れ、考えられるテンプレート引数に対してクラスを呼び出すすべてのクライアントからそのヘッダーを含めることをお勧めします。

// my_template.hpp
#include "my_template_fwd.hpp"
#include "my_template_primary.hpp"
#include "my_template_spec_some_type.hpp" 

// my_template_fwd.hpp
template<typename> class my_template; // forward declaration of the primary template

// my_template_primary.hpp
#include "my_template_fwd.hpp"
template<typename T> class my_template { /* full definition */ };

// my_template_spec_some_type.hpp
#include "my_template_fwd.hpp"
template<> class my_template<some_type> { /* full definition */ };

// some_client_module.hpp
#include "my_template.hpp" // no ODR possible, compiler will always see unique definition

明らかに、テンプレートの特殊化用のサブディレクトリを作成して名前を再編成し、それに応じてインクルード パスを変更できます。

于 2013-01-10T18:52:09.370 に答える