关于 volatile 限定符限定的成员函数的问题

jac*_*k X 6 c++ language-lawyer

#include <iostream>
struct A{
    A() = default;
    A(volatile const A&){}
    void show()const volatile {

    }
};
int main(){
   volatile A a;
   //A b = std::move(a);  // ill-formed
   std::move(a).show();  //OK
}
Run Code Online (Sandbox Code Playgroud)

考虑这个例子,这个例子的结果超出了我对一些相关规则的理解。

对于A b = std::move(a);,它是格式错误的,因为它违反了以下规则,即:
dcl.init.ref#5.2

否则,如果引用是对非 const 限定或volatile 限定的类型的左值引用,则程序格式错误。

这意味着,对 const volatile 限定的 T 的左值引用不能绑定到任何右值,即使它们是引用兼容的。 A b = std::move(a);显然违反了这条规则,因此它的格式不正确。

但是我不知道为什么编译std::move(a).show();而不报告错误。根据这个规则:

对于非静态成员函数,隐式对象参数的类型为

没有引用限定符或带有 & 引用限定符的函数声明的“对 cv X 的左值引用”

成员函数的隐式对象参数的类型show将为volatile const A& . 一般来说,它肯定违反了 [dcl.init.ref#5.2]。如果将成员函数的定义show改为:

void show() volatile const& {

}
Run Code Online (Sandbox Code Playgroud)

std::move(a).show();会畸形。所以必须在下面的规则中有一些魔法,std::move(a).show();在更改之前编译make show。规则是:
over.match.funcs#general-5

对于没有引用限定符声明的非静态成员函数,适用附加规则:

即使隐式对象参数不是 const 限定的,只要在所有其他方面参数可以转换为隐式对象参数的类型,右值也可以绑定到参数

老实说,我真的不知道“在所有其他方面”这个词是什么意思?“隐式对象参数的类型”指的是什么?“类型”是指volatile const A&还是被引用的类型volatile const A?措辞非常含糊。无论如何,对 const volatile T 的左值引用不能绑定到任何类型的右值T。那么,如何解读呢?

作为对比:

#include <iostream>
struct B{
  void show(){}
};
int main(){
  volatile B b;
  std::move(b).show();  //ill-formed
}
Run Code Online (Sandbox Code Playgroud)

的隐式对象参数的类型show将是B&,根据文献[over.match.funcs#一般-5],即使忽略const-qualifier,它仍然形成不良的,由于它丢弃volatile-qualifier。从这个例子中,它暗示,对于这句话“在所有其他方面,参数都可以转换为隐式对象参数的类型”,其中类型应该指的是引用类型而不是引用所指的类型。如果魔法是这样,它仍然不足以使std::move(a).show(); 形成良好的。

那么,如何解读这些问题呢?不知道怎么用[over.match.funcs#general-5]来解释这两个例子。

dfr*_*fri 2

struct A {
    A() = default;
    A(volatile const A &) {}
    void show() const volatile {}
};

int main() {
    volatile A a;
    std::move(a).show(); // OK
}
Run Code Online (Sandbox Code Playgroud)

show()根据[over.match.funcs]/4,成员函数的隐含对象参数是const volatile A&,为了重载决策的目的,我们可以根据[over.match.funcs]/5考虑数据成员函数为

void show(const volatile A&);
Run Code Online (Sandbox Code Playgroud)

现在,考虑到这一点,我们首先简化示例,目的是:

  • 比较为什么看似的右值引用A可以绑定到隐含的对象参数或类型const volatile A&,但当参数用于常规自由函数时,却不能绑定到相同类型的函数参数。

因此,请考虑以下简化示例:

#include <memory>

struct A {
    void show() const volatile {}
};

void g(const volatile A &) { }

int main() {
    volatile A a;
    
    g(std::move(a));      // (i) Error.
    std::move(a).show();  // (ii) OK.
}
Run Code Online (Sandbox Code Playgroud)

GCC (10.1.0) 中 (i) 处的错误消息是:

错误:无法将类型的非常量左值引用绑定到类型 {aka }const volatile A&的右值std::remove_reference<volatile A&>::typevolatile A

这是根据[dcl.init.ref]/5.2所期望的(正如您自己所指出的),它不允许右值在初始化时绑定到volatile引用,即使它们是const- 限定的。

那么为什么(ii)会被接受呢?或者,相反,为什么 [dcl.init.ref]/5.2 的限制显然不适用于成员函数的隐式对象参数的类似情况?

答案在于 [over.match.funcs ]/5.1 ,其中包含未使用 ref-qualifier声明的成员函数的异常:

[over.match.funcs ]/5 [...] 对于没有引用限定符声明的非静态成员函数,适用附加规则:

  • /5.1即使隐式对象参数不是 const 限定的,只要在所有其他方面可以将参数转换为隐式对象参数的类型,就可以将右值绑定到该参数。

[over.match.funcs ]/5.1 删除了 [dcl.init.ref]/5 关于右值(右值绑定)的禁止,其余标准应用于参数(右值被忽略;volatile A)是否可以(“在所有其他尊重”)转换为隐式对象参数(const volatile A&)。作为隐式对象参数,如上所示,在这种情况下始终是左值引用,这里“在所有其他方面”本质上意味着隐式对象参数是引用兼容的(根据[dcl.init.ref]/4)与(忽略右值)参数类型。

// [over.match.funcs ]/5.1 special case: rvalue prohibition waived
   volatile A a;                // argument: a
   const volatile A& aref = a;  // ok, reference-compatible
// ^^^^^^^^^^^^^^^^^ implicit object parameter
Run Code Online (Sandbox Code Playgroud)

可以说,[over.match.funcs]/5.1 可以更清楚地表明,它既适用于非const限定(通常)禁止 from-rvalue 绑定的情况,适用于volatile-cv-qualification(通常)禁止 from-rvalue 绑定的情况。


我们最终可以查询编译器这是否实际上是它用来允许的特定规则(ii),通过显式添加&-ref 限定符,根据[over.match.funcs]/4.1将进行的更改对隐式对象参数的类型没有影响:

#include <memory>

struct A {
    void show() const volatile & {}
};

void g(const volatile A &) { }

int main() {
    volatile A a;
    
    g(std::move(a));      // (i) Error.
    std::move(a).show();  // (ii') Error.
}
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,如果我们&show()重载添加 - 限定符,(ii) 同样会像 (i) 一样失败,尽管会出现另一个错误消息 (GCC):

错误:传递std::remove_reference<volatile A&>::type{aka volatile A} 作为this参数会丢弃限定符

对于这个错误,Clang (10.0.0) 可以说有更现场的错误消息:

错误: this成员函数的参数show是右值,但函数具有非常量左值引用限定符