You had stated:
But when the constructor is replaced with a function that returns void, this is legal.
However, I don't believe that it is: Here's a link to Compiler Explorer that demonstrates that your code fails to compile.
I've tested this with x86-64 clang 10.0.1
, x86-64 gcc 10.2
, and x64 msvc v19.24
and all three fail to compile. Here are the errors that they are each reporting:
- clang: - error: constructor
A
must not return void expression
- where the highlight is under the
return
keyword.
- gcc: - error: return a value from a constructor
- where the highlight is under the value
0
.
- msvc: - error C2534:
A
constructor cannot return a value
- where the highlight is under the entire line of code for that constructor.
This statement from your questions seems a bit misleading... I don't see how it is legal at all!
There are only 3 things a constructor can do...
- Early Return
- Throw an exception
- Return after
};
at the end of the constructor's block or scope is reached and control flow is returned back to the caller.
All functions must have a return type, even those that don't return such as:
void print(){/*...*/ return;}
int add(int a, int b) { return (a+b); }
- etc...
However, constructors
and destructors
are never declared with a type
.
You will never see:
struct A{
void A() { return; } // Fails to compile
int A() { return 0; } // Fails to compile
void ~A() { return; } // Fails to compile
int ~A() {return 0; } // Fails to compile
};
There are NO type
s associated with ctor
s and dtor
s, at least not in c++
.
Now if you remove the (void)0
within the code from Compiler Explorer you will see the foo():
and bar():
labels and their stack frames. You will also see the main:
label and its stack frame. Yet you will see nothing for A
. Now if you add an instance of A
in main by instantiating it with an instance of the class object, you will see the change in the assembly code within main's stack frame as it is local to the main()
function.
Here is clang's
assembly:
Without A a
being declared...
foo(): # @foo()
push rbp
mov rbp, rsp
pop rbp
ret
bar(): # @bar()
push rbp
mov rbp, rsp
xor eax, eax
pop rbp
ret
main: # @main
push rbp
mov rbp, rsp
xor eax, eax
mov dword ptr [rbp - 4], 0
pop rbp
ret
With A a;
being declared...
foo(): # @foo()
push rbp
mov rbp, rsp
pop rbp
ret
bar(): # @bar()
push rbp
mov rbp, rsp
xor eax, eax
pop rbp
ret
main: # @main
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 8]
call A::A() [base object constructor]
xor eax, eax
add rsp, 16
pop rbp
ret
A::A() [base object constructor]: # @A::A() [base object constructor]
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
pop rbp
ret
Here's gcc
's assembly:
Without:
foo():
push rbp
mov rbp, rsp
nop
pop rbp
ret
bar():
push rbp
mov rbp, rsp
mov eax, 0
pop rbp
ret
main:
push rbp
mov rbp, rsp
mov eax, 0
pop rbp
ret
With:
foo():
push rbp
mov rbp, rsp
nop
pop rbp
ret
bar():
push rbp
mov rbp, rsp
mov eax, 0
pop rbp
ret
A::A() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-1]
mov rdi, rax
call A::A() [complete object constructor]
mov eax, 0
leave
ret
Here's msvc
s assembly:
Without:
void foo(void) PROC ; foo
ret 0
void foo(void) ENDP ; foo
int bar(void) PROC ; bar
xor eax, eax
ret 0
int bar(void) ENDP ; bar
main PROC
xor eax, eax
ret 0
main ENDP
With:
void foo(void) PROC ; foo
ret 0
void foo(void) ENDP ; foo
int bar(void) PROC ; bar
xor eax, eax
ret 0
int bar(void) ENDP ; bar
this$ = 8
A::A(void) PROC ; A::A, COMDAT
mov QWORD PTR [rsp+8], rcx
mov rax, QWORD PTR this$[rsp]
ret 0
A::A(void) ENDP ; A::A
a$ = 32
main PROC
$LN3:
sub rsp, 56 ; 00000038H
lea rcx, QWORD PTR a$[rsp]
call A::A(void) ; A::A
xor eax, eax
add rsp, 56 ; 00000038H
ret 0
main ENDP
As you can see from all 3 compilers when you do not have an instance of an object there is no generated assembly code. When you do have an instance of an object all compilers invoke call
to A::A()
or A::A(void)
... Execution control enters into these constructors just like a function, however, they have no types because they are not actual functions, they are just treated like one...
They do have a stack frame, a scope, and a lifetime like a function, but these are only invoked when an object of the class
's or struct
's type is being declared. The assembly instructions for class
constructors are only generated when an instance is being created.
They are not like a regular function where you could do this:
foo() {
A(); // this is not valid
A::A(); // this is not valid
}
However, this is valid:
foo() {
A a(); // Valid
}
Here the constructor
is invoked on the object that is named
a
of type
A
.
I hope this helps to clarify why constructor
s or ctor
s don't have return types associated with them. The same thing goes for their destructor
s or dtor
s.
Edit
I think people were miss interpreting what I was trying to get at... I made a slight modification to the code: Maybe this will illustrate my intent more clearly...
Here's the C++ code:
struct A {
//A() { return (void)0; }
A() {};
};
void foo() {
A a;
return (void)0;
}
int bar() {
A a;
return 0;
}
int main() {
foo();
int baz = bar();
A a;
return 0;
}
And here's GCC's
version of its Assembly
:
foo(): # @foo()
push rbp
mov rbp, rsp
sub rsp, 16
lea rdi, [rbp - 8]
call A::A() [base object constructor]
add rsp, 16
pop rbp
ret
A::A() [base object constructor]: # @A::A() [base object constructor]
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
pop rbp
ret
bar(): # @bar()
push rbp
mov rbp, rsp
sub rsp, 16
lea rdi, [rbp - 8]
call A::A() [base object constructor]
xor eax, eax
add rsp, 16
pop rbp
ret
main: # @main
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 0
call foo()
call bar()
mov dword ptr [rbp - 8], eax
lea rdi, [rbp - 16]
call A::A() [base object constructor]
xor eax, eax
add rsp, 16
pop rbp
ret
Here's the udpated link to Compiler Explorer. And if you look closely at the generated assembly, When A::A()
is called there is no information, nor any assembly code in regards to a type. When foo()
is called its return type void
is optimized away and when bar()
is called there is assembly code to store it' return value.