Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: CWG2518適用を前提として記述する #1260

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
116 changes: 72 additions & 44 deletions lang/cpp17/if_constexpr.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,60 @@ void f(Out& o, A1 const& a1, A2 const& a2)
それでも記述が実際にしたい処理に比べて不必要に複雑になる。
constexpr if文の導入によりそのような複雑な手法を用いずに素直に条件付きのコンパイルを実現できるようになった。

`constexpr if`文の条件式内は実体化が起きる。したがって実体化するとコンパイルエラーになるものは書いてはいけない。

```cpp example
#include <type_traits>
#include <iostream>

struct Hoge {
using type = int;
};

template <typename T>
void f()
{
if constexpr (std::is_same_v<T::type, int> || std::is_same_v<T::value_type, int>) {
std::cout << "is int" << std::endl;
}
}

int main()
{
f<Hoge>(); //error: Hoge::value_typeは存在しないのでif constexpr文の条件式がコンパイルエラーになる
}
```

### `static_assert`文に関する例外

後に述べる2段階名前探索に関する注意点とは関係なく、C++23以降、もしくはCWG 2518が適用された環境においては、template文(もしくは適切な特殊化や`constexpr if`文の中の文)が実際にインスタンス化されるまで、`static_assert`文の宣言は遅延される。
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実際にインスタンス化されるまで

表記揺れ。インスタンス化→実体化

template文(もしくは適切な特殊化やconstexpr if文の中の文)

表記揺れ。template → テンプレート? ところで template 文って何のことかよくわからないので具体的にできますか。

CWG 2518 を確認したら、"テンプレート定義時に式が評価された場合は static_assert 宣言は効果がない" と言っていて、別に評価や検証が遅延されるわけではないみたいです。つまり「(2段階名前探索の) 1段階目で false になっても何も起こらない」という形みたいです。

static_assert文の宣言は遅延

「宣言」って書いている時は何を意味していますか? この文書の他の部分ではテンプレートの定義時点のことを宣言と言っているように見えますが、ここでは static_assert の「実体化」もしくは「検証」のことを言っている?

あと「static_assert文」ではなくて「static_assert宣言」のような…。

Suggested change
後に述べる2段階名前探索に関する注意点とは関係なく、C++23以降、もしくはCWG 2518が適用された環境においては、template文(もしくは適切な特殊化や`constexpr if`文の中の文)が実際にインスタンス化されるまで`static_assert`文の宣言は遅延される
後に述べる2段階名前探索に関する注意点とは関係なく、C++23以降、もしくはCWG 2518が適用された環境においては、template文(もしくは適切な特殊化や`constexpr if`文の中の文)が実際に実体化されるまで`static_assert`文の検証は遅延される

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

構文要素としてのstatic_assertは、文(statement)ではなく宣言(declaration)の一種ですね。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

で対応してみました。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

表記揺れ。template → テンプレート? ところで template 文って何のことかよくわからないので具体的にできますか。

これは対応する予定はありますか。P2593R1 の対応する記述に従えば "template 文" ではなくて "テンプレート” でしょうか (edit: lang/cpp11/static_assert.md にも同様のものがありますね)。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(元からあったみたいですが) テンプレート宣言時はテンプレート定義時の方がよいのではないかと思います。

あと、これについてもご確認いただけたらと思います。


```cpp example
#include <cstdint>
template <class T>
void f(T t) {
if constexpr (sizeof(T) == sizeof(std::int32_t)) {
use(t);
} else {
static_assert(false, "must be 32bit");
}
}

void g(std::int8_t c) {
std::int32_t n = 0;
f(n); // OK: nはstd::int32_t型なので`use(t);`のほうがインスタンス化されるために、static_assert文は宣言されない。
f(c); // error: cはstd::int8_t型なので、static_assert文は宣言され、"must be 32bit"とコンパイラが診断メッセージを出力する
}
```

### 2段階名前探索における注意点

`constexpr if`文で、実行されない方の`statement`は廃棄文(discarded statement)となり、文の実体化を防ぐ。言い換えると、2段階名前探索における依存名(dependent name)は、廃棄文の場合検証されない。また文が実体化されないのだから通常のif文と同じくもちろん実行時に実行もされない。つまり次の例は意図と異なる挙動を示す。

```cpp example
```cpp
template<bool> struct inferior_static_assert_failure;
template<> struct inferior_static_assert_failure<true>{ enum { value = 1 }; };
#define INFERIOR_STATIC_ASSERT(B) typedef char inferior_static_assert[sizeof(inferior_static_assert_failure<bool(B)>::value)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

後続趣旨「CWG 2518未適用のstatic_assertでも同じ問題がある」との直接対比可能な構造にしたいのだと推察しますが、
経緯や結論を知らない読者からすると、複雑な INFERIOR_STATIC_ASSERT マクロの唐突な登場は混乱を招かないでしょうか?

(とコメントしつつ、良い案が思い浮かばず申し訳ない)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

弱い提案:「つまり次の例は意図と異なる挙動を示す。」+INFERIOR_STATIC_ASSERT 利用例示を削除してしまっても、文意は壊れないと思いました。

(提案適用後の構成)

2段階名前探索における注意点
constexpr if文で、実行されない方のstatementは廃棄文(discarded statement)となり、文の実体化を防ぐ。言い換えると、2段階名前探索における依存名(dependent name)は、廃棄文の場合検証されない。また文が実体化されないのだから通常のif文と同じくもちろん実行時に実行もされない。CWG 2518が適用されていない環境においてはstatic_assertでも同じ現象が発生する。

(CWG 2518適用前の static_assert 問題点を指摘する例示コード)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INFERIOR_STATIC_ASSERTが唐突に過ぎるというのはそのとおりなんですが、これ相当のマクロを自作している古いプロジェクトをC++17対応させたあとに、中途半端に古いコードを良く知らずにコピペしてくるときに引っかかる落とし穴なのかなと思っていました(限定的すぎますよね・・・)。

提案についてはちょっと週末寝かせて考えてみます。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3da7c5d

除去することにしました。

#include <type_traits>

template <typename T>
Expand All @@ -82,7 +131,7 @@ void f(T)
{
// Tがintのときのみ評価されてほしい
// 実際は常に評価される
static_assert(false);
INFERIOR_STATIC_ASSERT(false);
}
}

Expand All @@ -93,21 +142,19 @@ int main()
}
```

なぜならば廃棄文はテンプレートの実体化を防ぐ (依存名の検証をしない) だけで、非依存名は検証されるからである。この例の[`static_assert`](/lang/cpp11/static_assert.md)に渡す条件式はテンプレートパラメータに依存していないので、テンプレートの宣言時に検証され、エラーとなる。言い換えれば`static_assert`に渡す条件式が依存名ならばテンプレートの宣言時に検証されず、テンプレート実体化まで評価を遅らせることができる
CWG 2518が適用されていない環境においては`static_assert`でも同じ現象が発生する

```cpp example
#include <type_traits>

template <typename T>
constexpr bool false_v = false;

template <typename T>
void f(T)
{
if constexpr (std::is_same_v<T, int>)
{
// Tがintのときのみ評価される
static_assert(false_v<T>);
// Tがintのときのみ評価されてほしい
// 実際は常に評価される
static_assert(false);
}
}

Expand All @@ -118,18 +165,25 @@ int main()
}
```

上の例では`false_v`を作ったが、ラムダ式でも同様のことができる。ラムダ式はそれが記述された位置から見て最小のスコープ (ブロックスコープ/クラススコープ/名前空間スコープ) で宣言されるクラスとして扱われる。例えば、下の例では`f()`という関数テンプレート内で宣言される。関数テンプレート内のクラスは依存名になるため、テンプレートの宣言時に検証されず、テンプレート実体化まで評価を遅らせることができる。
なぜならば廃棄文はテンプレートの実体化を防ぐ (依存名の検証をしない) だけで、非依存名は検証されるからである。この例の[`static_assert`](/lang/cpp11/static_assert.md)に渡す条件式はテンプレートパラメータに依存していないので、テンプレートの宣言時に検証され、エラーとなる。

#### CWG 2518が適用されていない環境でのワークアラウンド
yumetodo marked this conversation as resolved.
Show resolved Hide resolved

言い換えれば`static_assert`に渡す条件式が依存名ならばテンプレートの宣言時に検証されず、テンプレート実体化まで評価を遅らせることができる。

```cpp example
#include <type_traits>

template <typename T>
constexpr bool false_v = false;

template <typename T>
void f(T)
{
if constexpr (std::is_same_v<T, int>)
{
// Tがintのときのみ評価される
static_assert([]{return false;}());
static_assert(false_v<T>);
}
}

Expand All @@ -140,51 +194,25 @@ int main()
}
```

`constexpr if`文の条件式内は実体化が起きる。したがって実体化するとコンパイルエラーになるものは書いてはいけない
上の例では`false_v`を作ったが、ラムダ式でも同様のことができる。ラムダ式はそれが記述された位置から見て最小のスコープ (ブロックスコープ/クラススコープ/名前空間スコープ) で宣言されるクラスとして扱われる。例えば、下の例では`f()`という関数テンプレート内で宣言される。関数テンプレート内のクラスは依存名になるため、テンプレートの宣言時に検証されず、テンプレート実体化まで評価を遅らせることができる
Copy link
Member

@yohhoy yohhoy Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

弱い意見:古いコンパイラへに対する回避策提示は、P2593R1 で言及されている信頼性の高い方式1つで十分だと思います。

2.2 Workarounds
As a result of static_assert(false) not working, people turn to workarounds. Which are, basically: how can I write false in a sufficiently complex way so as to confuse the compiler?

Some of these workarounds actually still do run afoul of the above rule, but compilers aren’t smart enough to diagnose them, so they Just Happen To Work. Checks like: [...]

2.3 Valid Workaround
A more valid workaround is to have some template that is always false (e.g. from this answer):
static_assert(always_false<T>::value);
[...] This is really the only real workaround for this problem, and is something that people have to be taught explicitly as a workaround.

本件に関連する言語仕様解釈は難解なため、条件部にラムダ式を用いる遅延false実装の妥当性について、私個人(yohhoy)は確証をもてません。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

その記述はもともと constexpr if と local struct の評価のタイミングについてでの @yohhoy さんの回答がもとになっていた気がしますね・・・。cppreffjpに書くのを提案したのは私ですが・・・。

回避策として複数あげる必要性が薄らいだのは確かですが、積極的に除去する動機が自分にはないです。

ref: #577
cc: @alphya

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本コメントの趣旨は「C++言語リファレンスという特性を踏まえると、古い処理系向けの回避策として複数選択肢を提示し読者を迷わせる必然性はない」です。

PS. teratailサイトでの回答を引き合いに出されていますが、これは2018年当時の質問に答えただけです。この部分については私としては困惑しかありません。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

編集方針 - 正確なものに不要な情報はない という話も…。(追記: 個人的にはどっちつかずです)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ffb8d64

時間を開けて読み返した感じ、ないほうがスッキリするので思い切って消しました。うまくハマるように復活させられるならばそういう編集を後の人が行うことを止めるものではありません。


```cpp example
#include <type_traits>
#include <iostream>

struct Hoge {
using type = int;
};

template <typename T>
void f()
void f(T)
{
if constexpr (std::is_same_v<T::type, int> || std::is_same_v<T::value_type, int>) {
std::cout << "is int" << std::endl;
if constexpr (std::is_same_v<T, int>)
{
// Tがintのときのみ評価される
static_assert([]{return false;}());
}
}

int main()
{
f<Hoge>(); //error: Hoge::value_typeは存在しないのでif constexpr文の条件式がコンパイルエラーになる
}
```

### (CWG 2518が適用された環境) `static_assert`文に関する例外

上に述べたように、`constexpr if`文の中の文は廃棄文においても、非依存名の検証を行う。このため特に`static_assert`文を使う時に直感的ではない挙動を示していた。

C++23以降、もしくはCWG 2518が適用された環境においては、template文(もしくは適切な特殊化や`constexpr if`文の中の文)が実際にインスタンス化されるまで、`static_assert`文の宣言は遅延される。

```cpp example
#include <cstdint>
template <class T>
void f(T t) {
if constexpr (sizeof(T) == sizeof(std::int32_t)) {
use(t);
} else {
static_assert(false, "must be 32bit");
}
}

void g(std::int8_t c) {
std::int32_t n = 0;
f(n); // OK: nはstd::int32_t型なので`use(t);`のほうがインスタンス化されるために、static_assert文は宣言されない。
f(c); // error: cはstd::int8_t型なので、static_assert文は宣言され、"must be 32bit"とコンパイラが診断メッセージを出力する
f(2.4);
f(3);
}
```

Expand Down