web-dev-qa-db-ja.com

テンプレート引数の置換の順序が重要なのはなぜですか?

C++ 11

14.8.2 --テンプレート引数の推定 --[temp.deduct]

7 置換は、関数型およびテンプレートパラメータ宣言で使用されるすべての型と式で発生します。式には、配列の境界や非型テンプレート引数として表示されるような定数式だけでなく、sizeofdecltype、およびその他のコンテキスト内の一般式(つまり、非定数式)も含まれます。非定数式を許可します。


C++ 14

14.8.2 --テンプレート引数の推定 --[temp.deduct]

7 置換は、関数型およびテンプレートパラメータ宣言で使用されるすべての型と式で発生します。式には、配列の境界や非型テンプレート引数として表示されるような定数式だけでなく、sizeofdecltype、およびその他のコンテキスト内の一般式(つまり、非定数式)も含まれます。非定数式を許可します。 置換は辞書式順序で進行し、推論が失敗する条件が発生すると停止します



追加された文は、C++ 14でテンプレートパラメータを処理するときの置換の順序を明示的に示しています。

置換の順序は、ほとんどの場合、あまり注目されないものです。なぜこれが重要なのかについての論文はまだ1つも見つかりません。これは、C++ 1yがまだ完全に標準化されていないためかもしれませんが、そのような変更が何らかの理由で導入されたに違いないと思います。

質問:

  • テンプレート引数の置換の順序が重要なのはなぜ、いつですか?
56

前述のように、C++ 14は、テンプレート引数の置換の順序が明確に定義されていることを明示的に示しています。より具体的には、"辞書式順序で続行し、置換によって推論が失敗するたびに停止することが保証されます。

C++ 11と比較して、[〜#〜] sfinae [〜#〜]-C++ 14で別のルールに依存する1つのルールで構成されるコードを書くのがはるかに簡単になります。テンプレート置換の順序が未定義であると、アプリケーション全体が未定義の動作に悩まされる可能性がある場合。

:C++ 14で説明されている動作は、C++ 11でも常に意図された動作であり、そのような明示的な表現がされていないことに注意することが重要です。



そのような変化の背後にある理論的根拠は何ですか?

この変更の背後にある元の理由は、欠陥レポート元々DanielKrügler:によって提出された)にあります。


詳細説明

[〜#〜] sfinae [〜#〜]を書くとき、開発者はコンパイラに依存して、使用時にテンプレートで無効なtypeまたはexpressionを生成する置換を見つけます。そのような無効なエンティティが見つかった場合、テンプレートが宣言しているものはすべて無視し、適切な一致を見つけるために進みます。

置換の失敗はエラーではありません、しかし単なる.. "ああ、これは機能しませんでした..先に進んでください"

問題は、潜在的な無効な型と式が、置換の即時コンテキスト)でのみ検索されることです。

14.8.2 --テンプレート引数の推定 --[temp.deduct]

8 置換によって無効な型または式が生成された場合、型の推定は失敗します。無効な型または式は、置換された引数を使用して記述された場合に不正な形式になるものです。

[注:アクセスチェックは置換プロセスの一部として実行されます。 --end note]

関数型とそのテンプレートパラメータ型の直接のコンテキストで無効な型と式のみが推定の失敗につながる可能性があります。

[注:置換された型と式の評価は、クラステンプレートの特殊化や関数テンプレートの特殊化のインスタンス化、暗黙的に定義された関数の生成などの副作用を引き起こす可能性があります。このような副作用「即時のコンテキスト」にないため、プログラムの形式が正しくない可能性があります。 --end note]

言い換えると、非即時コンテキストで発生する置換は、プログラムの形式を崩します。そのため、テンプレート置換の順序が重要です。特定のテンプレートの全体的な意味が変わる可能性があります。

より具体的には、SFINAEで使用できるisのテンプレートと、isではないのテンプレートの違いである可能性があります。


愚かな例

template<typename SomeType>
struct inner_type { typedef typename SomeType::type type; };
template<
  class T,
  class   = typename T::type,            // (E)
  class U = typename inner_type<T>::type // (F)
> void foo (int);                        // preferred
template<class> void foo (...);          // fallback
struct A {                 };  
struct B { using type = A; };

int main () {
  foo<A> (0); // (G), should call "fallback "
  foo<B> (0); // (H), should call "preferred"
}

(G)とマークされた行で、コンパイラーに最初に(E)をチェックさせ、それが成功した場合は(F)を評価しますが、この投稿で説明する標準の変更前は、そのような保証はありませんでした。


foo(int)の置換の直接のコンテキストには以下が含まれます。

  • (E)渡されたT::typeがあることを確認します
  • (F)inner_type<T>::typeがあることを確認する


(F)が無効な置換をもたらしたとしても(E)が評価された場合、または(F)(E)の前に評価された場合、短い(愚かな)例ではSFINAEを使用せず、アプリケーションの形式が正しくないという診断が表示されます。ただし、そのような場合にfoo(...)を使用することを意図していました。


注:SomeType::typeがテンプレートの即時コンテキストにないことに注意してください。inner_type内のtypedefに失敗すると、アプリケーションの形式が正しくなくなり、テンプレートを使用できなくなります。 of [〜#〜] sfinae [〜#〜]



これはC++ 14でのコード開発にどのような影響を及ぼしますか?

この変更により、使用している準拠コンパイラに関係なく、特定の方法(および順序)で評価されることが保証されているものを実装しようとする言語弁護士)の寿命が劇的に短縮されます。

また、テンプレート引数の置換がnon-language-lawyers;に対してより自然な方法で動作するようになります。置換が左から右へ)から発生することは、erhm-like-anyよりもはるかに直感的です。 -way-the-compiler-wanna-do-it-like-erhm -...


否定的な意味はありませんか?

私が考えることができる唯一のことは、置換の順序は左から右)から発生するため、コンパイラーは非同期実装を使用して一度に複数の置換を処理することを許可されていないということです。

私はまだそのような実装に出くわすことはなく、それが大きなパフォーマンスの向上につながるとは思えませんが、少なくとも(理論的には)考えは物事の「ネガティブ」な側面に当てはまります。

例:コンパイラーは、必要に応じて、特定のポイントが発生しなかった後に発生した置換のように動作するメカニズムがないと、特定のテンプレートをインスタンス化するときに同時に置換を行う2つのスレッドを使用できません。



物語

:このセクションでは、テンプレート引数の置換の順序が重要になる時期と理由を説明するために、実際の生活から取った可能性のある例を示します。何かが十分に明確でない場合、またはおそらく間違っている場合は、(コメントセクションを使用して)私に知らせてください。

enumeratorsを使用していて、指定されたenumeration基礎となるを簡単に取得する方法が必要だと想像してください。

基本的に、理想的には(A)に近いものが必要なときに、常に(B)を記述しなければならないことにうんざりしています。

auto value = static_cast<std::underlying_type<EnumType>::type> (SOME_ENUM_VALUE); // (A)
auto value = underlying_value (SOME_ENUM_VALUE);                                  // (B)

元の実装

以上のように、以下のようにunderlying_valueの実装を作成することにしました。

template<class T, class U = typename std::underlying_type<T>::type> 
U underlying_value (T enum_value) { return static_cast<U> (enum_value); }

これは私たちの痛みを和らげ、私たちが望んでいることを正確に実行しているようです。列挙子を渡し、基になる値を取り戻します。

私たちは、この実装が素晴らしいことを自覚し、私たちの同僚(Don Quixote)に、本番環境にプッシュする前に、座って実装を確認するように依頼します。


コードレビュー

Don Quixoteは経験豊富なC++開発者で、片手にコーヒーを飲み、もう片方の手にC++標準を持っています。彼が両手を忙しくして1行のコードを書く方法は謎ですが、それは別の話。

彼は私たちのコードをレビューし、実装が安全ではないと結論付けました。列挙型ではないTを渡すことができるので、未定義の振る舞いからstd::underlying_typeを保護する必要があります。

20.10.7.6 --その他の変換 --[meta.trans.other]

template<class T> struct underlying_type;

条件:Tは列挙型でなければなりません(7.2)
コメント:メンバーtypedeftypeは、基になるタイプTに名前を付ける必要があります。

注:標準ではunderlying_typeに対して条件)が指定されていますが、non-enumでインスタンス化された場合に何が起こるかを指定することはこれ以上進みません。そのような場合に何が起こるかを知っている使用法は未定義の振る舞い;それは純粋な[〜#〜] ub [〜#〜]、アプリケーションを不正な形式にする、または食用に注文する)である可能性がありますオンライン下着。


輝く鎧の騎士

ドンは、私たちが常にC++標準をどのように尊重すべきか、そして私たちが行ったことに対して途方もない恥を感じるべきだと何かを叫びます。それは受け入れられません。

彼が落ち着き、コーヒーをもう少し飲んだ後、許可されていないものでstd::underlying_typeをインスタンス化することに対する保護を追加するように実装を変更することを提案します。

template<
  typename T,
  typename   = typename std::enable_if<std::is_enum<T>::value>::type,  // (C)
  typename U = typename std::underlying_type<T>::type                  // (D)
>
U underlying_value (T value) { return static_cast<U> (value); }

風車

Donの発見に感謝し、実装に満足していますが、テンプレート引数の置換の順序がC++ 11で明確に定義されていないことに気付くまでです(置換がいつ停止するかについても述べられていません)。

C++ 11としてコンパイルされた実装では、次の2つの理由により、enumerationtypeではないTstd::underlying_typeのインスタンス化が発生する可能性があります。

  1. 置換順序が明確に定義されていないため、コンパイラーは(D)の前に(C)を自由に評価できます。

  2. コンパイラが(C)の前に(D)を評価したとしても、(D)を評価しないという保証はありません。C++ 11には、置換チェーンをいつ停止する必要があるかを明示的に示す句がありません。


Donによる実装は、C++ 14ではndefined-behavior)から解放されますが、これは、C++ 14が置換が辞書式順序で進行するであり、停止する)と明示的に述べているためです。置換によって推論が失敗するときはいつでも

ドンはこれで風車と戦っていないかもしれませんが、彼は確かにC++ 11標準で非常に重要なドラゴンを逃しました。

C++ 11での有効な実装では、テンプレートパラメータの置換が発生する順序に関係なく、std::underlying_typeのインスタンス化が無効な型にならないようにする必要があります。

#include <type_traits>

namespace impl {
  template<bool B, typename T>
  struct underlying_type { };

  template<typename T>
  struct underlying_type<true, T>
    : std::underlying_type<T>
  { };
}

template<typename T>
struct underlying_type_if_enum
  : impl::underlying_type<std::is_enum<T>::value, T>
{ };

template<typename T, typename U = typename underlying_type_if_enum<T>::type>
U get_underlying_value (T value) {
  return static_cast<U> (value);  
}

注:underlying_typeが使用されたのは、標準にあるものに対して標準にあるものを使用する簡単な方法だからです。重要な点は、非列挙型でインスタンス化することは未定義の振る舞い)であるということです。

この投稿で以前にリンクされたdefect-report)は、この問題に関する広範な知識を前提とした、はるかに複雑な例を使用しています。

59