web-dev-qa-db-ja.com

このテンプレート関数が期待どおりに動作しないのはなぜですか?

私はテンプレート関数について読んでいて、この問題で混乱しました:

_#include <iostream>

void f(int) {
    std::cout << "f(int)\n";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << "  ";
    f(val);
}

void f(double) {
    std::cout << "f(double)\n";
}

template void g<double>(double);

int main() {
    f(1.0); // f(double)
    f(1);   // f(int)
    g(1.0); // d  f(int), this is surprising
    g(1);   // i  f(int)
}
_

template void g<double>(double);を記述しなくても、結果は同じです。

_g<double>_はf(double)の後にインスタンス化する必要があるため、f内のgへの呼び出しはf(double)を呼び出す必要があります。驚くべきことに、それは_g<double>_でf(int)を呼び出します。誰かがこれを理解するのを手伝ってくれる?


答えを読んだ後、私は私の混乱が本当に何であるかを理解しました。

これが更新された例です。 _g<double>_の特殊化を追加した以外は、ほとんど変更されていません。

_#include <iostream>

void f(int){cout << "f(int)" << endl;}

template<typename T>
void g(T val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

void f(double){cout << "f(double)" << endl;}

//Now use user specialization to replace
//template void g<double>(double);

template<>
void g<double>(double val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

int main() {
    f(1.0); // f(double)
    f(1);  // f(int)
    g(1.0); // now d  f(double)
    g(1);  // i  f(int)
}
_

ユーザーの特殊化により、g(1.0)は期待どおりに動作します。

のセクション26.3.3で説明されているように、コンパイラが_g<double>_の同じインスタンス化を同じ場所で(またはmain()の後でさえ)自動的に行わない場合 C++プログラミング言語、第4版)?

23
Zhongqi Cheng

名前fは依存名であり(引数Tを介してvalに依存します)、次のように解決されます 2つのステップ

  1. 非ADLルックアップは、テンプレート定義コンテキストから可視の関数宣言を調べます。
  2. ADLは、テンプレート定義コンテキストまたはテンプレートインスタンス化コンテキスト

void f(double)はテンプレート定義コンテキストからは見えず、ADLもそれを見つけられません because

基本型の引数の場合、関連する名前空間とクラスのセットは空です


あなたの例を少し修正することができます:

_struct Int {};
struct Double : Int {};

void f(Int) { 
    std::cout << "f(Int)";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << ' ';
    f(val);
    // (f)(val);
}

void f(Double) { 
    std::cout << "f(Double)";
}

int main() {
    g(Double{});
}
_

これで、ADLは2番目のステップでvoid f(Double)を検出し、出力は6Double f(Double)になります。 ::f(val)の代わりに_(f)(val)_(またはf(val))と書くことでADLを無効にできます。すると、出力は6Double f(Int)になり、例と一致します。

12
Evg

問題はf(double)が呼び出された時点で宣言されていないことです。その宣言をtemplate gの前に移動すると、呼び出されます。

編集:なぜ手動のインスタンス化を使用するのですか?

(関数テンプレートについてのみ説明しますが、クラステンプレートにも同様の引数が適用されます。)主な用途は、コンパイル時間を短縮したり、テンプレートのコードをユーザーから隠したりすることです。

C++プログラムは、コンパイルとリンクという2つのステップでバイナリに組み込まれます。関数呼び出しのコンパイルを成功させるには、関数のヘッダーのみが必要です。リンクを成功させるには、コンパイルされた関数の本体を含むオブジェクトファイルが必要です。

コンパイラーがtemplated関数の呼び出しを検出した場合、コンパイラーが何をするかは、テンプレートの本体を知っているか、ヘッダーのみを知っているかによって異なります。ヘッダーのみが表示される場合は、関数がテンプレート化されていない場合と同じことを行います。リンカーの呼び出しに関する情報をオブジェクトファイルに書き込みます。ただし、テンプレートの本文も参照する場合は、別のことも実行します。本文の適切なインスタンスをインスタンス化し、この本文をコンパイルして、オブジェクトファイルにも挿入します。

複数のソースファイルがテンプレート関数の同じインスタンスを呼び出す場合、それらの各オブジェクトファイルには、関数のインスタンスのコンパイル済みバージョンが含まれます。 (リンカーはこれを知っており、単一のコンパイル済み関数へのすべての呼び出しを解決するため、プログラム/ライブラリの最終バイナリには1つしかありません。)ただし、各ソースファイルをコンパイルするには、関数をインスタンス化し、コンパイルに時間がかかりました。

関数の本体が1つのオブジェクトファイル内にある場合、リンカはそれを行うだけで十分です。テンプレートをソースファイルに手動でインスタンス化することは、コンパイラに関数の本体を問題のソースファイルのオブジェクトファイルに挿入させる方法です。 (それは関数が呼び出されたかのようですが、インスタンス化は関数呼び出しが無効になる場所に書き込まれます。)これが完了すると、関数を呼び出すすべてのファイルは、関数のヘッダーのみを知ってコンパイルできるため、各呼び出しで関数の本体をインスタンス化してコンパイルするのにかかる時間を節約できます。

2番目の理由(実装の非表示)は、今では理にかなっているかもしれません。ライブラリ作成者がテンプレート関数のユーザーが関数を使用できるようにしたい場合は、通常、テンプレートのコードを提供して、自分でコンパイルできるようにします。テンプレートのソースコードを秘密にしたい場合は、ライブラリのビルドに使用するコードでテンプレートを手動でインスタンス化し、ソースの代わりに取得したオブジェクトバージョンをユーザーに提供できます。

これは意味がありますか?

6
AshleyWilkes