An issue about the member function qualified by volatile qualifier

Viewed 259
#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
}

Consider the example, the results of the example are out of my understanding about some relevant rules.

For A b = std::move(a);, it's ill-formed, because it violates the following rule, that is:
dcl.init.ref#5.2

Otherwise, if the reference is an lvalue reference to a type that is not const-qualified or is volatile-qualified, the program is ill-formed.

That means, a lvalue reference to const volatile-qualified T cannot bind to any rvalue even though they're reference-compatible. A b = std::move(a); obviously violates this rule, hence it's ill-formed.

However I don't know why compile std::move(a).show();without reporting wrong. According to this rule:

For non-static member functions, the type of the implicit object parameter is

“lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier

The type of the implicit object parameter of member function show will be volatile const A& . In general, it definitely violates [dcl.init.ref#5.2]. If change the definition of member function show to:

void show() volatile const& {

}

std::move(a).show(); will be ill-formed. So must be some magic in the following rule that make std::move(a).show(); be compiled before changing show. The rule is:
over.match.funcs#general-5

For non-static member functions declared without a ref-qualifier, an additional rule applies:

even if the implicit object parameter is not const-qualified, an rvalue can be bound to the parameter as long as in all other respects the argument can be converted to the type of the implicit object parameter.

Honestly, I really don't know what does the wording "in all other respects" mean? And what does the "the type of the implicit object parameter" refer to? Does the "type" refer to volatile const A& or the referenced type volatile const A? The wording is very vague. Anyhow, lvalue reference to const volatile T cannot bind to any rvalue of type T. So, how to interpret that?

As a contrast:

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

the type of the implicit object parameter of show would be B&, According to [over.match.funcs#general-5], even though ignore const-qualifier, it's still ill-formed due to it discards the volatile-qualifier. From this example, it implies that, For this sentence "in all other respects the argument can be converted to the type of the implicit object parameter", where the type should refer to reference type rather than the type the reference refers to. If the magic is this, it's still not sufficient to make std::move(a).show(); to be well-formed.

So, how to interpret these issues? I don't know how to use [over.match.funcs#general-5] to interpret these two examples.

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

int main() {
    volatile A a;
    std::move(a).show(); // OK
}

The implied object parameter for the member function show() is, as per [over.match.funcs]/4, const volatile A&, such that for the purpose of overload resolution, we may, as per [over.match.funcs]/5, consider the data member function as

void show(const volatile A&);

Now, with this in mind, let's first simplify the example, with the purpose of:

  • Comparing why an rvalue reference of A seemingly may bind to an implied object parameter or type const volatile A& but not to say a function parameter of the same type when the parameter is for a regular free function.

Thus, consider the following simplified example:

#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.
}

The error message at (i), in GCC (10.1.0) is:

error: cannot bind non-const lvalue reference of type const volatile A& to an rvalue of type std::remove_reference<volatile A&>::type {aka volatile A}

which is expected (as you have noted yourself) as per [dcl.init.ref]/5.2, which disallows rvalues to bind, in initialization, to volatile references, even if they are const-qualified.

Then why is (ii) accepted? Or, conversely, why does the restriction of [dcl.init.ref]/5.2 apparently not apply for the similar case of the implicit object parameter of a member function?

The answer lies in [over.match.funcs ]/5.1, which contains an exception for member functions declared without a ref-qualifier:

[over.match.funcs ]/5 [...] For non-static member functions declared without a ref-qualifier, an additional rule applies:

  • /5.1 even if the implicit object parameter is not const-qualified, an rvalue can be bound to the parameter as long as in all other respects the argument can be converted to the type of the implicit object parameter.

[over.match.funcs ]/5.1 removes the prohibation of [dcl.init.ref]/5 regarding rvalues (rvalue binding), and the remaining criteria is applied to whether argument (rvalue ignored; volatile A) can be ("in all other respects") converted to the implicit object parameter (const volatile A&). As the implicit object parameter, as shown above, in this case is always an lvalue reference, "in all other respects" here essentially means that the implicit object parameter is reference-compatible (as per [dcl.init.ref]/4) with the (rvalue ignored) argument type.

// [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

Arguably [over.match.funcs]/5.1 could be clearer on that it applies both for the case where non-const qualification (usually) prohibits from-rvalue binding, and that volatile-cv-qualification (usually) prohibits from-rvalue binding.


We may finally query the compiler(s) whether this is actually the particular rule which it uses to allow (ii), by explicitly adding the &-ref-qualifier, a change that as per [over.match.funcs]/4.1 will have no effect on the type of the implicit object parameter:

#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.
}

As expected, if we add a &-qualifier to the show() overload, (ii) likewise fails as (i) did, albeit with another error message (GCC):

error: passing std::remove_reference<volatile A&>::type {aka volatile A} as this argument discards qualifiers

For this error, Clang (10.0.0) arguably has a more on-spot error message:

error: this argument to member function show is an rvalue, but function has non-const lvalue ref-qualifier

Related