2

This is a follow-up to my previous post

With reference to Non-static member functions

Under

const-, volatile-, and ref-qualified member functions

A non-static member function can be declared with no ref-qualifier,... During overload resolution, non-static cv-qualified member function of class X is treated as follows:

no ref-qualifier: the implicit object parameter has type lvalue reference to cv-qualified X and is additionally allowed to bind rvalue implied object argument

To explore this further, I experimented with the source code provided in the above link, as shown below:

#include <utility>
#include <iostream>
using std::move;
using std::cout;
using std::endl;

struct S {
    void f() {cout << "no ref-qualifier: the implicit object parameter has type lvalue reference to cv-qualified S and is additionally allowed to bind rvalue implied object argument\n"; }

    //if only the below method signature were to be enabled,
    //the invocations using rvalue implicit object would fail
    //to compile with the error [-fpermissive]
    //void f() & {cout << "lvalue\n"; }

    //if only the below method signature were to be enabled,
    //the invocation using lvalue implicit object would fail
    //to complile with the error [-fpermissive]
    //void f() && {cout << "rvalue\n"; }
};

int main (void){

    S s;
    s.f();       // prints "lvalue"
    move(s).f(); // prints "rvalue"
    S().f();          // prints "rvalue"

    return 0;
}

I have provided relevant comments above each non-static member function overload based on reference qualifier, highlighting the compilation issue that would ensue if only that particular overload were to be enabled, in view of the source code in main().

My question is, what is happening under the hoods in order for the non-ref qualified non-static member function to be able to be agnostic to the implicit type of the object upon which it has been invoked? Does the compiler step in with the appropriate overload?

Appreciate your thoughts.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
Vinod
  • 925
  • 8
  • 9

1 Answers1

0

Const- and ref-qualified member functions do not function differently from non-const/ref member functions. These markings are mostly there to stop programmers from misusing stuff.

No that object is const because you are not supposed to change it, so you are not allowed to call a mutating function on it!

  • compiler yelling at programmer.

If you have a function void g() const, then it does not suddenly compile to different instructions because you remove the const from it - it just means that the compiler stops checking if you mutate this in the body.

Well, mostly... It gets a bit more complicated by the fact that, by upholding various invariants, these keywords also sometimes allow the programmer (or compiler) to do stuff that would not be allowed in the general case. For example, if I know you have given me an rvalue, then I am allowed to steal its innards instead of needing to copy them.

In any case, to illustrate, I ran your three examples through godbolt.

In the first case void f() compiled to:

S::f():
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov QWORD PTR [rbp-8], rdi
  mov esi, OFFSET FLAT:.LC0
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
  nop
  leave
  ret

You don't have to be able to read all that, just see that when I compiled the two other functions they produced this:

S::f() &:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        leave
        ret

S::f() &&:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        leave
        ret

Just so we are clear, except for loading different string literals (.LC0, .LC1), they are all three identical. And if we add -O3, then it all gets inlined into main as such:

No ref-qualifiers:

main:
  sub rsp, 8
  mov esi, OFFSET FLAT:.LC0
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
  mov esi, OFFSET FLAT:.LC0
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
  mov esi, OFFSET FLAT:.LC0
  mov edi, OFFSET FLAT:_ZSt4cout
  call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
  xor eax, eax
  add rsp, 8
  ret
_GLOBAL__sub_I_main:
  sub rsp, 8
  mov edi, OFFSET FLAT:_ZStL8__ioinit
  call std::ios_base::Init::Init() [complete object constructor]
  mov edx, OFFSET FLAT:__dso_handle
  mov esi, OFFSET FLAT:_ZStL8__ioinit
  mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
  add rsp, 8
  jmp __cxa_atexit

With ref-qualifiers:

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

Which again is identical (except for string literals)

In short: Nothing has to be done to make the non-ref-qualified function accept both arguments - it is the other way around: The compiler stops the ref-qualified functions from accepting arguments that they technically could but are not allowed to take (both to guard against programmers doing stuff they should not do, and in case the function was optimized to do something that would actually break on the general case).

On some level it is like the type system: On the machine level the computer is just moving around bits and bytes, and gives absolutely zero fucks about what type you think any given chunk represents. It is purely the compiler trying to hold you to account and make sure you uphold the invariants that you declared, but once the compiler is happy and emits some machine code then the types are long gone.

Or, since I like examples, you can say that it is like the difference between the VIP and the non-VIP door: The doors actually identical, but the guard (compiler) only lets you enter through the ones that (they think) you have permission to.

Frodyne
  • 3,547
  • 6
  • 16