非定数定数式

// <コード>



int main

{

constexpr int a = f ;

constexpr int b = f ;



static_assert a != b "fail" ;

}



上記のフラグメントにコメントの代わりにf()の定義を挿入して aがb以外の値を取得することは可能ですか?



「もちろんそうではありません!」あなたは少し考えて言う。 実際、両方の変数はconstexpr指定子で宣言されます 。つまり、 f()constexpr関数でなければなりません。 constexpr関数はコンパイル時に実行できることを誰もが知っています。その結果、プログラムのグローバルな状態に依存したり、プログラムを変更したりするべきではありません(言い換えると、 それらはcleanでなければなりません )。 清浄度とは、関数が同じ引数で呼び出されるたびに同じ値を返す必要があることを意味します。 f()は引数なしで両方の時間で呼び出されるため、変数abに割り当てられる同じ値を両方の時間で返す必要があります...



1週間前、私はこれが真実であることを知っていたので、未定義の動作を避けて、上記のスニペットでstatic_assertを渡すことは不可能だと本当に思っていました。



私は間違っていました。



内容





免責事項:この記事で説明する手法は、「C ++の暗い深みに没頭する別のトリッキーなハック」と位置付けられています。 最初にすべての落とし穴(以下の記事で詳細に説明します)を学習せずに、実稼働環境で使用することはお勧めしません。



免責事項は、他のサイトでこの記事のコメントを読んだ後に追加されたもので、その目的の誤解を見つけました。 私はあなたの寝室の外で説明されたテクニックを使用することを承認しません(そのような使用の結果を考慮しないで)。




なぜこれが必要なのでしょうか?



記事の冒頭で提起された問題を解決したら、コンパイルプロセスに可変状態を正常に追加できます(言い換えると、命令型メタプログラムを記述できます)。



ご注意 trans:可変定数定数を「変数」として使用すると、メタプログラムの命令性を簡単に追加できると読者は主張するかもしれません。 したがって、問題を解決するには、 #define f()__COUNTER__またはそのようなものを使用できます。



残念ながら、これは最も単純な場合にのみ機能します。これは、プリプロセッサがプログラムテキスト内でマクロに遭遇するとすぐにマクロを展開し、言語自体の構文構造を無視するためです。 したがって、メタ関数のインスタンス化の順序が重要になるか、変数を一度読み取って変更する以外の何かが必要になるとすぐに、マクロはその能力で役に立たなくなります。


一見、これは小さくて無邪気な概念のように思えるかもしれませんが、この記事で説明する手法を使用すると、以前は非常に複雑なコードを必要としたり、まったく解決できなかった多くの問題を解決できます。



例:



コンパイル時カウンター



C1 = ... を使用します



int constexpr a = C1 :: next ; // = 1

int constexpr b = C1 :: next ; // = 2

int constexpr c = C1 :: next ; // = 3


コンパイル時メタタイプコンテナー



LX = ... を使用します



LX :: push < void void void void > ;

LX :: set < 0 クラス Hello > ;

LX :: set < 2 クラス World > ;

LX :: pop ;



LX :: <> x ; // type_list <class Hello、void、class World>


他のアイデア







予備情報



注:このパートでは、問題の解決に関連する技術的な側面について詳しく説明します。 したがって、最も経験豊富な(またはせっかちな)読者はそれをスキップできます。 お菓子のためだけにここにいる場合は、すぐに決定に進みます。
注:少なくとも、ソリューションコードが正確にどのように、なぜ機能するのか(そして標準の観点からは合法です)に興味がある場合は、少なくともこの部分をざっと読むことをお勧めします。


友達キーワード



C ++のフレンドシップ関係は、別のエンティティにそのプライベートメンバーおよび保護されたメンバーへのアクセスを提供するだけでなく使用できます。 次の(非常に単純な)例について考えます。



クラス A ;

ボイドタッチ A ;



クラス A

{

友人 ボイドタッチ A ;

intメンバー;

} ;



ボイドタッチ A ref

{

ref。 メンバー = 123 ; // OK、 `void touch(A&)`はクラスAの友人です

}



int main

{

A a ; a。 メンバー = 123 ; //無効、メンバー「A ::メンバー」プライベート

A b ; タッチ b ;

}


最初に、グローバルスコープでvoidタッチ(A&)関数を宣言し、次にクラスAに フレンドリに宣言し、最後にグローバルスコープで定義します。



同じ成功を収めて、次の例のように、何も変更せずに、クラスA内にvoid touch(A&)の宣言と定義を組み合わせて直接配置できます。



クラス A

{

フレンド ボイドタッチ A ref

{

ref。 メンバー = 123 ;

}



intメンバー;

} ;



int main

{

A b ; タッチ b ; // OK

}


friendを使用するためのこれら2つのアプローチが完全に同等はないことは非常に重要です(ただし、この特定のケースではそうであるように思われるかもしれません)。



最後の例では、 ボイドタッチ(A&)は、クラスAを囲む最も近い名前空間のスコープに暗黙的に配置されますが、 Koenig検索 (ADL)によってのみアクセスできます。



クラス A

{

公開

A int ;



フレンド ボイドタッチ A { ... }

} ;



int main

{

A b = 0 ; タッチ b ; // OK、 `void touch(A)`はADLを介して検出されます

タッチ 0 ; //間違っています、引数の型は `int`で、ADLはクラスをスキャンしません` A`

}


以下の標準からの抜粋は上記を繰り返しますが、行間に書かれていることに集中してほしいです(同様に重要だからです):



7.3.1.2/3名前空間 メンバー定義 [namespace.memdef] p3
名前空間内で最初に宣言された各名前は、その名前空間のメンバーです。 非ローカルクラス内のフレンド宣言が最初にクラス、関数、クラステンプレート、または関数テンプレートを宣言する場合、それらは最も近くにある名前空間のメンバーになります。 フレンド宣言だけでは、非修飾(3.4.1)または修飾(3.4.3)の名前検索で名前が表示されることはありません。


フレンド宣言を介して入力された名前は、この宣言が置かれているクラスの名前と、実際にはこのクラスとの関係に関係している必要があることはどこにも記載されていないことに注意してください。



#include <iostream>



クラス A

{

公開

A int { }

友人 ボイド f A ;

} ;



ボイド g A ;



クラス B

{

友達 ボイド f A

{

std :: cout << "hello world!" << std :: endl ;

}



クラス C

{

友達 ボイド g A

{

std :: cout << "!dlrow olleh" << std :: endl ;

}

} ;

} ;



int main

{

A a 0 ;

f a ;

g 1 ;

}


したがって、 f()はクラスB関係がありません。ただし、クラスBは 、その内部で直接フレンドとして定義されているという事実を除き、そのようなコードは絶対に正しいです。



注:これらの考慮事項は非常に簡単に思えるかもしれませんが、何が起こっているのか疑問がある場合は、コンパイラーを見つけて上記のスニペットを試してみることをお勧めします。


定数式のルール



constexprに関連するルールは多数あり、この導入をより厳密かつ詳細にすることができますが、簡単に説明します。





注:このリストは不完全で厳密ではありませんが、ほとんどの場合にconstexprエンティティと定数式がどのように動作するかを示しています。


定数式の操作のすべての側面を詳細に検討する代わりに、呼び出し時にまだ定義されていない関数呼び出しを定数式に含めないようにする規則に焦点を当てます。



constexpr int f ;

ボイドインダイレクション ;



int main

{

constexpr int n = f ; //無効、 `int f()`はまだ定義されていません

インダイレクション ;

}



constexpr int f

{

0を 返し ます

}



無効な間接指定

{

constexpr int n = f ; // OK

}


ここでは、 constexpr -function int f()宣言 れていますが、 main定義されていません 、インダイレクション内に定義があります(ボディが開始するまでにインダイレクション定義がすでに提供されているため)。 したがって、 イン ダイレクション内では、 constexpr関数f()の呼び出しは定数式になり、 nの初期化は正しくなります。



式が定数であることを確認する方法



特定の式が定数であるかどうかを確認する方法はいくつかありますが、それらのいくつかは他のものよりも実装が困難です。



経験豊富なC ++開発者は、ここでSFINAE (置換の失敗はエラーではない)の概念をうまく適用する機会をすぐに見て、正しいでしょう。 しかし、SFINAEが提供する能力は、かなり複雑なコードを記述する必要性と結びついています。



ご注意 あたり:
たとえば、
constexpr int x = 7 ;



テンプレート < typename >

std :: false_type isConstexpr ... ;



template < typename T T test = 15 * 25 - x > //テスト式

std :: true_type isConstexpr T * ;



constexpr bool value = std :: is_same < decltype isConstexpr < int > nullptr std :: true_type > :: value ; // true


noexcept演算子を使用して問題を解決する方がはるかに簡単です。 この演算子は、例外をスローできない式に対してtrueを返し、そうでない場合はfalseを返します 。 特に、すべての定数式は例外をスローしないと見なされます。 これでプレイします。



constexpr int f ;

ボイドインダイレクション ;



int main

{

// `f()`は定数式ではありません。

//その定義が欠落している間



constexpr bool b = noexcept f ; // false

}



constexpr int f

{

0を 返し ます

}



無効インダイレクション

{

//そして今



constexpr bool b = noexcept f ; // true

}


注:現在、 clangにはバグが含まれています。これは、チェックされた式が定数であっても、 noexcepttrueを返さないためです 。 回避策は、この記事の付録に記載されています。


テンプレートのインスタンス化のセマンティクス



C ++標準に、ほとんどのプログラマにとって本当の挑戦をもたらす部分がある場合、それは間違いなくテンプレートに関連しています。 ここでテンプレートのインスタンス化のあらゆる側面を検討することにした場合、記事は非常に大きくなり、少なくとも数時間は読み続けることになります。



これは私たちが目指していることではないので、代わりに、問題の解決策がどのように機能するかを理解するために必要なインスタンス化の基本原則についてのみお話ししようとします。



注:このセクションの内容は、テンプレートをインスタンス化するための完全なリファレンスではないことに注意してください。 それに記載されているルールには例外があります。さらに、記事の範囲を超えているいくつかの事実を意図的に省略しました。


基本原則



最小の辞書
  • テンプレートの特殊化は、テンプレートパラメータを特定の引数に置き換えることにより、テンプレートから取得される実装です。 テンプレート<typename T>クラスFoo-テンプレート。 Foo <int>は彼の専門分野です。 プログラマは、その動作が一般化されたものと異なることが必要な場合、特定の引数セットに対してテンプレートの完全または部分的な特殊化を個別に提供できます。 関数テンプレートでは部分的な特殊化は使用できません。
  • テンプレート特化のインスタンス -汎用テンプレートコードから特殊化コードをコンパイラーで取得します。 簡潔にするために、彼らはしばしば特定の引数を使用してテンプレートをインスタンス化することについて話し、「特殊化」という言葉を省略します。
  • インスタンス化は明示的または暗黙的です。 明示的にインスタンス化する場合、プログラマーは特定の引数を使用してテンプレートをインスタンス化する必要があることをコンパイラーに個別に通知します。例: template class Foo <int> さらに、必要に応じて、コンパイラーはそれ自体で特殊化を暗黙的にインスタンス化できます。


以下は、タスクに最も直接関係する原則の簡潔なリストです。





インスタンス化ポイント



クラスまたは関数のテンプレートの特化が言及されているコンテキストがそのインスタンス化を必要とするときはいつでも、いわゆる インスタンス化ポイント (実際には、汎用テンプレートコードから特殊化コードを生成するときにコンパイラが存在できる場所の1つを決定します)。



ネストされたテンプレートの場合、外部テンプレートYのパラメーターに依存するコンテキストで内部テンプレートの特殊化Xが言及されている場合、この特殊化のインスタンス化ポイントは外部テンプレートの対応するポイントに依存します。





ネストされたテンプレートがない場合、またはコンテキストが外部テンプレートのパラメーターに依存しない場合、インスタンス化ポイントは、特殊化Xが言及された「最もグローバルな」エンティティ宣言/定義のポイントDに関連付けられます。





関数テンプレート特化生成



関数テンプレートの特殊化には、任意の数のインスタンス化ポイントを含めることができますが、コンパイラーがどのインスタンス化ポイントを選択してコードを生成しても、テンプレート内の式は同じ意味を持つ必要があります(そうでない場合、プログラムは正しくないと見なされます)



簡単にするために、C ++標準では、インスタンス化された関数テンプレートの特殊化には、 翻訳単位の最後に追加のインスタンス化ポイントがあることを規定しています。



名前空間 N

{

struct X { / *特別に空白のまま* / }



void func X int ;

}



テンプレート < typename T >

void call_func T val

{

func val 3.14f ;

}



int main

{

call_func N :: X { } ;

}



//最初のインスタンス化ポイント



名前空間 N

{

float func X float ;

}



// 2番目のインスタンス化ポイント


この例では、関数void call_func <N :: X>(N :: X)のインスタンス化の2つのポイントがあります。 最初はmainの定義の直後( call_funcはその内部で呼び出されるため)で、2番目はファイルの最後です。



call_func <N :: X>の動作は、特殊化コードを生成するコンパイラーに応じて変化するため、この例は正しくありません。





クラステンプレート専門化の生成



クラステンプレートを特化するために、最初のものを除くすべてのインスタンス化ポイントは無視されます。 これは、実際には、インスタンス化が必要なコンテキストでコンパイラが最初に言及されたときに、コンパイラが特殊化コードを生成する必要があることを意味します。



名前空間 N

{

struct X { / *特別に空白のまま* / }



void func X int ;

}



template < typename T > struct A { using type = decltype func T { } 3.14f ; } ;

template < typename T > struct B { type = decltype func T { } 3.14f )を使用して ; } ;



//インスタンス化ポイントA



int main

{

A < N :: X > a ;

}



名前空間 N

{

float func X float ;

}



//インスタンス化ポイントB



ボイド g

{

A < N :: X > :: タイプ a ; //間違っています、タイプは `void`になります

B < N :: X > :: タイプ b ; // OK、タイプは「float」になります

}


ここでは、インスタンス化ポイントA <N :: X>mainの直前になり、インスタンス化ポイントB <N :: X>は gの直前になります。



すべてをまとめる



クラステンプレート内のフレンド宣言に関連付けられたルールは、次の定義例ではfunc(short)func(float)が生成されそれぞれ特殊化A <short>A <float>のインスタンス化ポイントに配置されることを示しています。



constexpr int func short ;

constexpr int func float ;



テンプレート < typename T >

構造体 A

{

友達 constexpr int func T { 0を 返す ; }

} ;



テンプレート < typename T >

A < T >インダイレクション

{

return { } ;

}



//(1)



int main

{

インダイレクション< short > ; //(2)

インダイレクション< float > ; //(3)

}


計算式に関数テンプレートの特殊化の呼び出しが含まれる場合、この特殊化の戻り値の型を完全に定義する必要があります。 したがって、行(2)および(3)の呼び出しは、インスタンス化ポイント(順番にmainの前)の前に、 間接化特殊化とともに特殊化Aの暗黙的なインスタンス化を必要とします。



行(2)および(3)に到達する前に、関数func(short)およびfunc(float)が 宣言 されているが、 定義されていないことが重要です。 これらの行の達成により、特殊化Aのインスタンス化が発生する場合、これらの関数の定義は表示されますが、これらの行の隣ではなく、ポイント(1)に配置されます。





解決策



予備的な情報が、このセクションで説明するソリューションで使用される言語のすべての側面を十分に明らかにすることを願っています。



念のために、ソリューションがどのように、なぜ機能するかを完全に理解するために、次の側面についてのアイデアが必要であることを思い出させてください。





実装



constexpr int flag int ;



テンプレート < typenameタグ>

構造ライター

{

友達 constexpr intフラグタグ

{

0を 返し ます

}

} ;



テンプレート < bool B typename Tag = int >

struct dependent_writer writer <タグ> { } ;



テンプレート <

bool B = noexcept flag 0

int = sizeof dependent_writer < B >

>

constexpr int f

{

リターン B ;

}



int main

{

constexpr int a = f ;

constexpr int b = f ;



static_assert a != b "fail" ;

}


注: clangはこのコードで不正な動作を示します 。回避策はアプリケーションで利用できます
ご注意 trans: Visual Studio 2015の視覚的なキューは、 f()の変更にも「気付かない」。 ただし、コンパイル後、 abの値は異なります。


しかし、それはどのように機能しますか?



予備情報を読んだ場合、上記の解決策を理解することで問題が生じることはありませんが、その場合でも、さまざまな部分の動作原理の詳細な説明が興味深い場合があります。



このセクションでは、ソースコードをステップごとに分析し、各フラグメントについて簡単な説明と正当性を示します。



「変数」



プログラムの各ポイントで、 constexpr関数は2つの状態のいずれかになります。既に定義されており、定数式から呼び出すことができるかどうかです。 これら2つの状況のいずれかのみが可能です(あいまいな動作を許可しない限り)。



通常、 constexpr関数は関数と見なされ、正確に使用されますが、上記のおかげで、 boolに似た型を持つ「変数」と考えることができます。 そのような各「変数」は、「定義済み」または「未定義」の2つの状態のいずれかです。



constexpr int flag int ;


このプログラムでは、 フラグ関数は同様のトリガーです。 どこでも関数として呼び出すのではなく、「変数」としての現在の状態にのみ関心があります。



修飾子



writerは、インスタンス化されると、その名前空間(この場合はグローバル)で関数の定義を作成するクラステンプレートです。 タグテンプレートパラメータは、定義が作成される関数の特定のシグネチャを定義します。



テンプレート < typenameタグ>

構造ライター

{

友達 constexpr intフラグタグ

{

0を 返し ます

}

} ;


予定通り、 constexpr -functionsを「変数」と見なす場合、テンプレート引数Tを使用し ライターを作成すると、「変数」のシグネチャint func(T)が「定義済み」位置に無条件に変換されます。



プロキシ



テンプレート < bool B typename Tag = int >

struct dependent_writer writer <タグ> { } ;


dependent_writerがインダイレクションを追加する無意味なレイヤーのように見えると決めても驚くことではありません。 dependent_writerを介してアクセスするのではなく、「変数」の値を変更するライター<Tag>を直接インスタンス化しないのはなぜですか?



事実、 ライター<int>への直接呼び出しは、関数テンプレートfの最初の引数が2番目よりも早く評価されることを保証しません(そして、関数は、最初の呼び出しでfalseを返す必要があることを「記憶」し、その後で「変数」の値を変更するだけです) 。



必要なテンプレート引数を計算する順序を設定するには、dependent_writerを使用して追加の依存関係を追加できます。 最初のdependent_writerテンプレート引数は、インスタンス化される前に評価される必要があるため、 ライターがインスタンス化される前に評価する必要があります。 したがって、 Bdependent_writerに引数として渡すと、 ライターがインスタンス化されるまでに、 fによって返される値がすでに計算されていることを確認できます。



注:実装を作成する際に、多くの代替案を検討し、最も簡単に理解できるものを見つけようとしました。 この断片があまりにも混乱しないことを心から願っています。


魔法



テンプレート <

bool B = noexcept flag 0 //(1)

int = sizeof dependent_writer < B > //(2)

>

constexpr int f

{

リターン B ;

}


このスニペットは少し奇妙に見えるかもしれませんが、実際には非常に簡単です:





動作は、次の擬似コードで表現できます。



 [ `int flag (int)`    ]:  `B` = `false`  `dependent_writer <false>`  `B` :  `B` = `true`  `dependent_writer <true>`  `B`
      
      





したがって、最初にfが呼び出されると、テンプレート引数Bfalseになりますが、 fを呼び出す副作用は「変数」 フラグの状態の変化になります(その定義を生成してbody mainの前に配置します)。 さらにfを呼び出すと 「変数」 フラグはすでに「定義済み」状態になっているため、 Btrueになります





おわりに



C ++(以前は不可能と考えられていた)で新しいことを行うためのクレイジーな方法を人々が発見し続けているという事実は、驚くべきものであり、ひどいものです。 - モーリス・ボス


この記事では、定数式に状態を追加する基本的な考え方について説明します。 言い換えれば、定数表現は「定数」であるという一般に受け入れられている理論(私がよく参照した)は、今では破壊されています。



ご注意 trans:私の意見では、「破壊された」という言葉は非常に強力です。 同じように、名前の同一性にもかかわらず、テンプレート関数f()の 2つの異なる特殊化がソリューションで呼び出され、それぞれが完全に「一定」です。 もちろん、これはアイデアの一般的な有用性を損なうものではありません。


この記事を書いているとき、テンプレートメタプログラミング歴史と、この言語を使用すると、意図した以上のことができるようになるのがどれほど奇妙か考えずにはいられませんでした。

次は?



smetaと呼ばれる命令型テンプレートメタプログラミング用のライブラリを作成し ました 。これは、今後の記事で公開、説明、および議論されます。 取り上げるトピックの中で:





ご注意 trans:著者は、 smetaのリリースをキャンセルすることを決めたと報告しています。 この記事および後続の記事には、その機能のほとんどすべてが含まれている(または含まれる)ため、読者が独自に機能を実装すること自体は、ささいなことです。 たとえば、私はすでに(Philipの警告に反して)いくつかのアイデアを紹介しており、将来それらをライブラリのようなものに集めるつもりです。




アプリ



clangこの (および関連する)バグのため、上記のソリューションでは、正しく実装されている場合、プログラムが正しく動作しません。 以下は、 clang向けに特別に作成されたソリューションの代替実装です(これはまだ有効なC ++コードであり、どのコンパイラでも使用できますが、多少複雑です)。



名前空間の詳細

{

構造体 A

{

constexpr A { }

友達 constexpr int adl_flag A ;

} ;



テンプレート < typenameタグ>

構造ライター

{

friend constexpr int adl_flag タグ

{

0を 返し ます

}

} ;

}



template < typename Tag int = adl_flag Tag { } >

constexpr bool is_flag_usable int

{

trueを返します

}



テンプレート < typenameタグ>

constexpr bool is_flag_usable ...

{

falseを返します

}



テンプレート < bool B クラス Tag = detail :: A >

struct dependent_writer detail :: writer <タグ> { } ;



テンプレート <

class Tag = detail :: A

bool B = is_flag_usable <タグ> 0

int = sizeof dependent_writer < B >

>

constexpr int f

{

リターン B ;

}



int main

{

constexpr int a = f ;

constexpr int b = f ;



static_assert a != b "fail" ;

}


注:現在、この回避策がclangで効率的である理由を示す対応するバグレポートを書いています。 それらへのリンクは、レポートが提出されるとすぐに追加されます(すべてが鈍い間- 約Per。 )。


元の記事の感謝セクション

謝辞



この記事を書かない人はたくさんいますが、特に感謝しています。



  • モーリス・ボス
    • アイデアの策定を支援します。
    • コンタクトレンズを購入するための資金を提供してくれたので、盲目的に書く必要がなくなりました。
    • 記事に対する彼の意見に対する私の嫌がらせの許容。
  • マイケル・キルペライネン
    • 証拠、およびこの記事をより理解しやすくする方法に関する興味深い考え。
  • コロンボ
    • 説明した手法が不正なプログラムを生成することを証明しようとして失敗した(私の顔にC ++標準の段落を投げて)。 どちらかといえば、私は彼のために同じことをします。




翻訳者から



私はこの記事に約1か月前に出くわし、テンプレートのメタプログラミングに機能性、グローバル状態への依存性を追加できるアイデア恵みのひどいわいせつさに感銘を受けました。 この記事の著者、 Philippe Rosenは、開発者、ミュージシャン、ファッションモデルであり、良い人でしたが、親切にロシア語に翻訳することを許可してくれました。



この記事は、命令型メタプログラミングに関する一連の記事の最初の記事であり、実際のメタプログラミングをはるかに便利にする構成を実装する前に、非定数constexprs (実際の即時適用性は非常に限られています)のアイデアを開発します。



近い将来、残りの2つの記事(コンパイル時カウンターとメタタイプのコンテナーについて)を翻訳します。 さらに、フィリップは、近い将来、シリーズを継続し、さらにアイデアを発展させると述べました(もちろん、新しい翻訳があります)。



訂正、追加、希望は大歓迎です。 フィリップ自身に手紙書くこともできます。彼はどんなコメントでも喜んでくれると思います。






All Articles