13

ライブラリで const および非 const テンプレート引数の関数を提供しようとすると、奇妙な問題に遭遇しました。次のソース コードは最小限の現象の例です。

#include <iostream>


template<typename some_type>
struct some_meta_class;

template<>
struct some_meta_class<int>
{
    typedef void type;
};



template<typename some_type>
struct return_type
{
    typedef typename some_meta_class< some_type >::type test;

    typedef void type;
};



template<typename type>
typename return_type<type>::type foo( type & in )
{
    std::cout << "non-const" << std::endl;
}

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


int main()
{
    int i;

    int const & ciref = i;
    foo(ciref);
}

非 const バージョンと foo の const バージョンを実装しようとしましたが、残念ながらこのコードは CLANG 3.0 と gcc 4.6.3 ではコンパイルできません。

main.cpp:18:22: エラー: 未定義のテンプレート 'some_meta_class' の暗黙的なインスタンス化

そのため、何らかの理由で、コンパイラは const int 参照に foo の非 const バージョンを使用したいと考えています。some_meta_class の実装がないため、これは明らかに上記のエラーにつながります。奇妙なことに、次の変更のいずれかを行うと、コードが正常にコンパイルされて機能します。

  • 非constバージョンのコメントを外す/削除する
  • return_type::test の typedef を uncomemnt/削除します

もちろん、この例は最小限であり、純粋に学術的なものです。私のライブラリでは、const バージョンと非 const バージョンが異なる型を返すため、この問題に遭遇しました。部分的に特殊化されたヘルパー クラスを使用して、この問題を管理しました。

しかし、なぜ上記の例でこのような奇妙な動作が発生するのでしょうか? コンパイラは、const バージョンが有効であり、よりよく一致する非 const バージョンを使用したくないのはなぜですか?

4

1 に答える 1

24

その理由は、関数呼び出しの解決が実行される方法と、テンプレート引数の演繹と置換です。

  1. まず、名前検索が実行されます。これにより、名前が一致する 2 つの関数が得られますfoo()

  2. 次に、推定が実行されます。名前が一致する各テンプレート関数について、コンパイラは実行可能な一致を生成する関数テンプレート引数を推定しようとします。エラーはこのフェーズで発生します。

  3. 第三に、オーバーロードの解決がゲームに入ります。これは型推定が実行され、呼び出しを解決するための実行可能な関数のシグネチャが決定された後でのみ行われます。これは理にかなっています。コンパイラは、すべての候補の正確なシグネチャを見つけた後にのみ、関数呼び出しを意味のある方法で解決できます。

非 const オーバーロードに関連するエラーが発生するという事実は、コンパイラが呼び出しを解決するための最も実行可能な候補としてそれを選択するためではなく (ステップ 3)、戻り値の型をインスタンス化するときにコンパイラがエラーを生成するためです。ステップ 2 で署名を決定します。

ただし、これがエラーになる理由は完全には明らかではありません。なぜなら、 SFINAEが適用されると予想されるからです (置換の失敗はエラーではありません)。これを明確にするために、より簡単な例を考えてみましょう。

template<typename T> struct X { };

template<typename T> typename X<T>::type f(T&) { }  // 1
template<typename T> void f(T const&) { }           // 2

int main()
{
    int const i = 0;
    f(i); // Selects overload 2
}

この例では、SFINAE が適用されます。ステップ 2 で、コンパイラはT上記の 2 つのオーバーロードのそれぞれを推測し、それらのシグネチャを特定しようとします。オーバーロード 1 の場合、これは代入の失敗という結果になります:X<const int>何も定義しませんtype(no typedefin X)。ただし、SFINAE のため、コンパイラは単純にそれを破棄し、オーバーロード 2 が実行可能な一致であることを検出します。したがって、それを選択します。

あなたの例を反映するように、例を少し変更しましょう。

template<typename T> struct X { };

template<typename Y>
struct R { typedef typename X<Y>::type type; };

// Notice the small change from X<T> into R<T>!
template<typename T> typename R<T>::type f(T&) { }  // 1
template<typename T> void f(T const&) { }           // 2

int main()
{
    int const i = 0;
    f(i); // ERROR! Cannot instantiate R<int const>
}

変更されたのは、オーバーロード 1 が返されなくなりX<T>::type、むしろ返されることR<T>::typeです。これは、 で宣言されているため、と同じであるため、同じ結果が得られると予想される場合があります。ただし、この場合、コンパイル エラーが発生します。なんで?X<T>::typetypedefR

基準には答えがあります (パラグラフ 14.8.3/8):

置換の結果が無効な型または式になる場合、型推定は失敗します。無効な型または式は、置換された引数を使用して記述した場合に不適切な形式になるものです。[...]関数型とそのテンプレート パラメーター型の直接のコンテキストで無効な型と式のみが、推論の失敗につながる可能性があります。

明らかに、2 番目の例 (およびあなたの例) はネストされた contextでエラーを生成するため、SFINAE は適用されません。これがあなたの質問に答えると思います。

ところで、これがより一般的に述べられているC++03 以降に変更されたことに注目するのは興味深いことです(段落 14.8.2/2):

[...] テンプレート パラメーターまたは関数テンプレートの関数型での置換が無効な型になる場合、型推定は失敗します。[...]

物事が変化した理由に興味がある場合は、このホワイトペーパーを参考にしてください。

于 2013-01-31T14:37:17.767 に答える