mad*_*ada 9 c++ language-lawyer c++20
给出以下代码:
struct A {
constexpr static auto e() { return 0; }
void f(int V1 = e()) { int V2 = e(); }
};
Run Code Online (Sandbox Code Playgroud)
我收到以下错误:
error: function 'e' with deduced return type cannot be used before it is defined
void f(int V1 = e()) { int V2 = e(); }
~~^~~
Run Code Online (Sandbox Code Playgroud)
我的理解是,函数体和默认参数都是完整的类上下文,其中类及其成员定义在这些上下文中被认为是完全定义的。
在函数体内,我使用该函数A::e作为变量的初始值设定项V2。此时没有错误,这使我确信在函数体内,该函数A::e被认为是完全定义的。
在函数参数列表中,我使用了 A::eparameter 的默认参数V1。根据定义,默认参数被视为完整的类上下文。此时,我收到一条错误,指出A::e在定义之前已使用该错误。但根据我的理解,A::e 它被认为是在任何完整的类上下文中定义的。
这对我来说有点奇怪,因为函数体和默认参数是完整的类上下文。我真的没有看到两者之间有任何区别,因为在这两种情况下,成员函数X::e都是完全定义的。
我不确定[dcl.spec.auto]/11是导致此处错误的原因。它说:
如果具有未推导的占位符类型的实体的名称出现在表达式中,则程序格式不正确 [..]
如果[dcl.spec.auto]/11是导致错误的原因,则变量的初始化V2也应是格式错误的,因为在这种情况下,e初始化程序的名称e()是具有未推导的占位符类型的实体的名称。
所以,我的问题是e():禁止成为V1 和 的默认参数,同时允许e()成为 的初始值设定项的标准措辞是什么V2?
正如OP所建议的,没有明显的理由说明为什么处于完整类上下文中应该保证您可以使用具有推导返回类型的函数。
OP 写道:
我的理解是,函数体和默认参数都是完整的类上下文,其中类及其成员定义在这些上下文中被认为是完全定义的。
根据 [class.mem.general]/7,在完整类上下文中,类被视为完整类型。它没有说明所有函数体是否“可见”,因此返回类型推导已经完成。(我将这个术语放在引号中,因为我没有以标准方式使用它,也不会尝试给出严格的定义。)
确实,在类的成员函数体内,我们可以调用类中稍后声明的其他函数,这是通过 [class.mem.general]/7 和名称的组合实现的查找规则([basic.lookup.unqual]/8)。然而,这与被调用者的主体“可见”无关。毕竟,它的定义甚至可能在另一个 TU 中。
当存在相互递归时,处于完整类上下文中显然是不够的:
struct B {
static auto e() { return decltype(f())(0); }
static auto f() { return decltype(e())(0); }
};
Run Code Online (Sandbox Code Playgroud)
很明显,没有编译器会接受上面的代码。因此,当您处于完整类上下文中时,推导返回类型的其他成员函数的返回类型被认为是已知的,这一想法肯定不止于此。
事实上,我们甚至不需要相互递归。所有编译器都会拒绝以下内容:
struct C {
void f() { int V2 = e(); }
static auto e() { return 0; }
};
Run Code Online (Sandbox Code Playgroud)
前面的例子表明编译器按如下方式解释类定义内定义的函数:将所有类成员函数的声明提升到所有定义之上(就好像所有定义都是外联的),但保持它们之间的相对顺序。也就是说,上面的代码大约被解释为我们有一个单遍编译器和以下代码:
struct C {
void f();
static auto e();
};
void C::f() { int V2 = e(); }
auto C::e() { return 0; }
Run Code Online (Sandbox Code Playgroud)
当涉及默认参数时,它们在哪里定义?如果转换如下,则代码应该可以接受:
struct A {
constexpr static auto e();
void f(int V1);
};
constexpr auto A::e() { return 0; }
void A::f(int V1 = e()) { int V2 = e(); } // OK
Run Code Online (Sandbox Code Playgroud)
但是,如果编译器使用将所有默认参数放在所有函数体之前的策略,那么我们又会遇到问题:
struct A {
constexpr static auto e();
void f(int V1);
};
void A::f(int V1 = e()); // ill-formed
constexpr auto A::e() { return 0; }
void A::f(int V1) { int V2 = e(); }
Run Code Online (Sandbox Code Playgroud)
编译器不接受 OP 代码的事实表明,后一种解释与这些编译器内部发生的情况相对应。为什么四大巨头都选择将所有默认参数放在所有函数体之前?嗯,在 C++98 中,这是一个方便的选择。函数体可以包含对另一个成员函数的调用,并且该调用可以使用后者的默认参数的值。但反之则不然:在 C++98 中,虽然默认参数的初始值设定项可能涉及对另一个成员函数的调用,但后一个函数的主体并不要求已被看到(同样,它甚至可以被定义)在另一个 TU 中)。
我能听到你说“等一下,等一下,这是一个‘语言律师’问题!给我看看措辞!标准中的哪里规定了这种提升行为?”
事实并非如此。因为在C++98中,没有必要。该标准只是说该类被视为完整的内部函数体(包括构造函数初始化程序)和默认参数([class.mem]/2),并让编译器弄清楚要做什么。在 C++98 中,OP 的心理模型是“这是一个完整的类上下文,因此我可以完全访问有关所有成员类型的信息”。
后来标准中引入constexpr和auto导致了措辞上的差距。正如上面的例子B所示,OP的心智模型不再成立,程序的格式良好性似乎取决于默认参数是否在函数体之前或反之亦然的考虑,而标准没有产生任何指导!
然而,这个问题有一个解决方案。首先观察到所有编译器都接受模板化版本:
template <class T>
struct D {
constexpr static auto e() { return 0; }
void f(int V1 = e()) { }
};
int main() {
D<void>{}.f();
}
Run Code Online (Sandbox Code Playgroud)
为什么会这样呢?这是因为类模板特化的成员(注意:从D<void>技术上讲,是标准术语中的“特化”,即使类模板D尚未“特化” void)单独进行实例化(通常,这不会与类定义本身的实例化同时发生)。当f()被调用时,并且只有在那时, isD<void>::f的默认参数才会被实例化,因为此时需要它(C++20 [temp.inst]/3,[temp.inst]/13)。该默认参数的实例化会触发 (C++20 [temp.inst]/4) 定义的实例化D::e。当此代码被模板化时,一切都会顺利进行,因为标准要求仅在需要时才实例化模板化实体,并且无论声明顺序如何,都会发生这种情况;可以这么说,编译器必须解决依赖关系。
这种不一致是CWG 2335的主题。该示例与OP的示例不太相似,因为静态数据成员初始值设定项不是完整的类上下文。然而,最后的注释的含义非常广泛:
CWG 的共识是通过在需要时(而不是在类结束时)“实例化”延迟解析区域来同等对待模板和类。
非模板类中的“延迟解析区域”是什么?据推测,它只是意味着一个完整的类上下文。这里的见解是,为了回答诸如 OP 的代码是否应该工作之类的问题,我们需要将实例化的概念应用于非模板。完整的类上下文,例如函数体或默认参数,应该仅在需要时实例化。这意味着当这个核心问题最终得到解决时,OP 的代码(大概)将通过与模板版本的类比而变得格式良好。
| 归档时间: |
|
| 查看次数: |
841 次 |
| 最近记录: |