web-dev-qa-db-ja.com

C ++テンプレート関数はヘッダーでコンパイルされますが、実装はされません

テンプレートを学習しようとしていますが、この交絡エラーに遭遇しました。ヘッダーファイルでいくつかの関数を宣言していますが、関数を定義する別の実装ファイルを作成したいと考えています。ヘッダー(dum.cpp)を呼び出すコードは次のとおりです。

_#include <iostream>
#include <vector>
#include <string>
#include "dumper2.h"

int main() {
    std::vector<int> v;
    for (int i=0; i<10; i++) {
        v.Push_back(i);
    }
    test();
    std::string s = ", ";
    dumpVector(v,s);
}
_

ここで、動作するヘッダーファイル(dumper2.h)を次に示します。

_#include <iostream>
#include <string>
#include <vector>

void test();

template <class T> void dumpVector( std::vector<T> v,std::string sep);

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
    typename std::vector<T>::iterator vi;

    vi = v.begin();
    std::cout << *vi;
    vi++;
    for (;vi<v.end();vi++) {
        std::cout << sep << *vi ;
    }
    std::cout << "\n";
    return;
}
_

実装あり(dumper2.cpp):

_#include <iostream>
#include "dumper2.h"

void test() {
    std::cout << "!olleh dlrow\n";
}
_

奇妙なことに、dumpVectorを定義するコードを.hから.cppファイルに移動すると、次のエラーが発生します。

_g++ -c dumper2.cpp -Wall -Wno-deprecated
g++ dum.cpp -o dum dumper2.o -Wall -Wno-deprecated
/tmp/ccKD2e3G.o: In function `main':
dum.cpp:(.text+0xce): undefined reference to `void dumpVector<int>(std::vector<int, std::allocator<int> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: ld returned 1 exit status
make: *** [dum] Error 1
_

それでは、なぜそれが一方の方向で機能し、他方の方向では機能しないのでしょうか?コンパイラは明らかにtest()を見つけることができますが、なぜdumpVectorを見つけられないのですか?

27
flies

あなたが抱えている問題は、コンパイラがインスタンス化するテンプレートのバージョンを知らないということです。関数の実装をx.cppに移動すると、それはmain.cppとは異なる変換単位にあり、main.cppはそのコンテキストに存在しないため、特定のインスタンス化にリンクできません。これは、C++テンプレートの既知の問題です。いくつかの解決策があります:

1)以前と同じように、定義を.hファイルに直接配置します。これには、問題の解決(pro)、コードの読みやすさの低下、一部のコンパイラーでのデバッグの困難化(con)、コードの膨張(con)の増加など、長所と短所があります。

2)実装をx.cppに入れ、#include "x.cpp"内からx.hを入れます。これがファンキーで間違っているように思われる場合は、#includeが指定されたファイルを読み取ってコンパイルするだけであることに注意してくださいそのファイルがx.cppの一部であるかのように言い換えると、これは上記のソリューション#1とまったく同じことを行います。しかし、それはそれらを別々の物理ファイルに保持します。この種のことを行うときは、#includedファイルをそれ自体でコンパイルしようとしないことが重要です。このため、通常、これらの種類のファイルにはhpp拡張子を付けて、hファイルおよびcppファイルと区別します。

ファイル:dumper2.h

#include <iostream>
#include <string>
#include <vector>

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);
#include "dumper2.hpp"

ファイル:dumper2.hpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;

}

3)問題は、dumpVectorの特定のインスタンス化が、それを使用しようとしている翻訳単位に認識されていないことであるため、テンプレートが定義されているのと同じ翻訳単位で特定のインスタンス化を強制できます。 。これを追加するだけで:template void dumpVector<int>(std::vector<int> v, std::string sep); ...テンプレートが定義されているファイルに。これを行うと、hppファイル内からhファイルを#includeする必要がなくなります。

ファイル:dumper2.h

#include <iostream>
#include <string>
#include <vector>

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);

ファイル:dumper2.cpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;
}

template void dumpVector<int>(std::vector<int> v, std::string sep);

ちなみに、全体として、テンプレート関数はvectorby-valueを使用しています。これを行いたくない場合は、参照またはポインタで渡すか、またはイテレータを渡して、ベクトルの一時的なコピーを行わないようにします。

36
John Dibling

これは、exportキーワードが実現するはずだったものです(つまり、テンプレートをexportingすることで、ヘッダーではなくソースファイルに配置できます。残念ながら、コンパイラは1つだけです(Comeau)exportを完全に実装したことがある。

他のコンパイラ(gccを含む)がそれを実装しなかった理由については、理由は非常に単純です。exportを正しく実装するのは非常に難しいためです。コード内部テンプレートは、テンプレートがインスタンス化されるタイプに基づいて、(ほぼ)完全に意味を変更できるため、テンプレートのコンパイル結果の従来のオブジェクトファイルを生成できません。たとえば、x+yは、intでインスタンス化されるとmov eax, x/add eax, yのようなネイティブコードにコンパイルされますが、std::stringなどのオーバーロードされるoperator+などでインスタンス化されると、関数呼び出しにコンパイルされます。 [SOMECODE] _。

テンプレートの個別のコンパイルをサポートするには、2フェーズ名前ルックアップと呼ばれるものを実行する必要があります(つまり、テンプレートのコンテキストで名前をルックアップしますandテンプレートがインスタンス化されているコンテキストで)。また、通常、コンパイラーに、任意のタイプのコレクションに対するテンプレートのインスタンス化を保持できる、ある種のデータベース形式にテンプレートをコンパイルさせます。次に、コンパイルとリンクの間の段階でデータベースをチェックし、必要なすべてのタイプでインスタンス化されたテンプレートのコードが含まれていない場合は、コンパイラを再度呼び出します(必要に応じてリンカーに組み込むことができます)。必要なタイプでインスタンス化します。

極度の努力、実装の欠如などにより、委員会はC++標準の次のバージョンからexportを削除することを投票しました。 exportが意図したことの少なくとも一部を提供する、他の2つのかなり異なる提案(モジュールと概念)が作成されましたが、より有用で(少なくとも期待される)方法で実装するのが合理的です。

10
Jerry Coffin

テンプレートパラメータはコンパイル時に解決されます。

コンパイラーは.hを見つけ、dumpVectorに一致する定義を見つけて、それを保管します。この.hのコンパイルが完了しました。次に、ファイルの解析とコンパイルを続行します。 .cppのdumpVector実装を読み取るときは、まったく異なるユニットをコンパイルしています。 dumper2.cppでテンプレートをインスタンス化しようとするものは何もないため、テンプレートコードは単にスキップされます。コンパイラーは、後でリンカーに役立つ何かがあることを期待して、テンプレートのすべての可能なタイプを試行するわけではありません。

次に、リンク時に、型intのdumpVectorの実装がコンパイルされていないため、リンカは何も検出しません。したがって、このエラーが表示されるのはなぜですか。

export キーワードは、この問題を解決するように設計されていますが、残念ながらそれをサポートしているコンパイラはほとんどありません。したがって、定義と同じファイルで実装を維持してください。

5

テンプレート関数は実関数ではありません。コンパイラーは、テンプレート関数の使用に遭遇すると、その関数を実際の関数に変換します。したがって、テンプレート宣言全体がDumpVectorの呼び出しを検出するスコープ内にある必要があります。そうでない場合、実際の関数を生成できません。
驚くべきことに、多くのC++イントロ本はこれを間違っています。

2
Tim Kay

これはまさにC++でのテンプレートの動作方法であり、実装をヘッダーに配置する必要があります。

テンプレート関数を宣言/定義すると、コンパイラはテンプレートを使用する特定のタイプを魔法のように知ることができないため、通常の関数の場合のように.oファイルに入れるコードを生成できません。代わりに、そのインスタンス化の使用を検出したときに、型の特定のインスタンス化を生成することに依存しています。

したがって、実装が.Cファイルにある場合、コンパイラーは基本的に「このテンプレートのユーザーはいないので、コードを生成しないでください」と言います。テンプレートがヘッダーにある場合、コンパイラーはmainでの使用を確認し、実際に適切なテンプレートコードを生成できます。

1
Mark B

標準では技術的に許可されていますが、ほとんどのコンパイラでは、テンプレート関数の定義を別のソースファイルに置くことはできません。

参照:

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.12

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.14

0
Cogwheel