0

The following example does not meet the standard:

void f();

struct A {
    A() { 
        return f(); // should be replaced to `f(); return;`
    }
};

But when the constructor is replaced with a function that returns void, this is legal.

I know this is required by the standard, as follows:

12.1 Constructors

12 No return type (not even void) shall be specified for a constructor. A return statement in the body of a constructor shall not specify a return value.

But why?

Twice
  • 95
  • 4
  • I believe I understand what you are asking, however, the manner of your presentation seems a bit misleading. Perhaps you may show an example after your statement, "But when the constructor is replaced with a function that returns void, this is legal." to demonstrate your intent just for clarity purposes... I think that might help to where I can rephrase my answer. I wasn't trying to state that you are `wrong` not directly at least. I was only trying to illustrate the behaviors of `ctor`s through the use of their generated assembly. Then trying to elaborate on why they are generated that way. – Francis Cugler Aug 24 '20 at 04:47

2 Answers2

0

A constructor is a special method that is designed to initialize a new instance of that class. Under the hood they are not actually called (see this question), so really any return would be inaccurate since there really nothing is returned. Nothing here is different than void, because void is a type, and returning a type in a block of code which does not return would be confusing and misleading syntax.

Further, constructors are called as part of initialization and only writes the values of the arguments to sections of memory in the same way writing int n = 5, writes the value 5 to a block of memory which is referenced when n is used.

To the user the initialization process seems like it is just a function call, but is in reality a completely different process.

joshmeranda
  • 3,001
  • 2
  • 10
  • 24
-2

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 types associated with ctors and dtors, 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 msvcs 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 constructors or ctors don't have return types associated with them. The same thing goes for their destructors or dtors.


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.

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
  • There is nothing in your 'compiler explorer' that disproves the OP's quoted assertion. – user207421 Aug 23 '20 at 04:15
  • @MarquisofLorne How can something be legal if it fails to compile? – Francis Cugler Aug 23 '20 at 06:20
  • Simple. You haven't checked the OP's assertion at all. The assertion (that you even quoted) is for *regular* functions. I.e. `void foo() { return void(); }` - And that compiles just fine – StoryTeller - Unslander Monica Aug 23 '20 at 11:54
  • @StoryTeller-UnslanderMonica I was referring specifically to `ctor`'s and not `regular` functions. I showed the difference between the two... and how and when the compilers are generating the assembly for the constructors. The standalone functions are always there, but the constructors which are "special" functions are generated only when an object is created. – Francis Cugler Aug 23 '20 at 18:20
  • @FrancisCugler That's only because you defined it `inline`. Define the constructor out-of-line and it's in the assembly: https://godbolt.org/z/KsE51r – Artyer Aug 24 '20 at 01:37
  • @Artyer True, that's some of the optimization features of inlining functions and constructors, that helps to generate fewer assembly instructions. If you don't define the constructor as inline and define it outside of the body of the class either in a header or in a `cpp` file then sure it will be a part of that translation unit. In that case, the assembly code has to be generated in order for the function or `ctor` to be defined. I was inlining the code only to illustrate that with the generated assembly two things... – Francis Cugler Aug 24 '20 at 04:30
  • @Artyer ... with it being defined inline, the first is that the assembly will only be generated when there is an instantiation of an object `T` where there must be a `call` to `T::T()`, the other point that I was trying to illustrate is that by closely looking at the generated assembly is that there is absolutely no assembly code for any `class` `ctor` that pertains to a `return` `type`. The bottom line is that we treat `ctor`s and `dtor`s like they are functions since they behave similarly to normal functions, but in truth, they are completely different constructs or entities... – Francis Cugler Aug 24 '20 at 04:35
  • @Artyer ... They are bound to the object `T` through the use of the `::` the scope resolution operator, not in the sense of a `member` of `T` directly such as a member variable or a member function, but indirectly as a member of `T` that is used to compose, initialize, and decompose the object respectively. – Francis Cugler Aug 24 '20 at 04:39