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.