Arkanil Paul
Arkanil Paul

Reputation: 147

Why assembly differ when derived class method calls base class implementation of pure virtual method versus implementing it directly in derived class?

Code 1:

#include <iostream>

struct Interface
{
    virtual void pr_fn() = 0;
    virtual void pr_fn2() = 0;
    virtual void pr_fn3() = 0;
};

struct Base : Interface
{
    void pr_fn2() final
    {
        std::cout << "Base\n";
    }
};

struct Derived : Base
{
    void pr_fn() final
    {
        std::cout << "Derived2\n";
    }

    void pr_fn3() final
    {
        pr_fn2(); pr_fn();
    }
};

int main()
{
    Derived d;
    d.pr_fn3();
    return 0;
}

Code 2:

#include <iostream>

struct Interface
{
    virtual void pr_fn() = 0;
    virtual void pr_fn2() = 0;
    virtual void pr_fn3() = 0;
};

void Interface::pr_fn3()
{
    pr_fn2();
    pr_fn();
}

struct Base : Interface
{
    void pr_fn2() final
    {
        std::cout << "Base\n";
    }
};

struct Derived : Base
{
    void pr_fn() final
    {
        std::cout << "Derived\n";
    }

    void pr_fn3() final
    {
        Interface::pr_fn3();
    }
};

int main()
{
    Derived d;
    d.pr_fn3();
    return 0;
}

Code 1 assembly: (Compiler: x86-64 gcc 14.2, flags: -O3)

.LC0:
        .string "Base\n"
.LC1:
        .string "Derived2\n"
main:
        sub     rsp, 8
        mov     edx, 5
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edx, 9
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        xor     eax, eax
        add     rsp, 8
        ret

Code 2 assembly: (Compiler: x86-64 gcc 14.2, flags: -O3)

.LC0:
        .string "Base\n"
Base::pr_fn2():
        mov     edx, 5
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
.LC1:
        .string "Derived\n"
Derived::pr_fn():
        mov     edx, 8
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
Derived::pr_fn3():
        sub     rsp, 8
        mov     edx, 5
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edx, 8
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        add     rsp, 8
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
Interface::pr_fn3():
        push    rbx
        mov     rax, QWORD PTR [rdi]
        mov     rbx, rdi
        mov     rax, QWORD PTR [rax+8]
        cmp     rax, OFFSET FLAT:Base::pr_fn2()
        jne     .L7
        mov     edx, 5
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     rax, QWORD PTR [rbx]
        mov     rax, QWORD PTR [rax]
        cmp     rax, OFFSET FLAT:Derived::pr_fn()
        jne     .L9
.L11:
        mov     edx, 8
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        pop     rbx
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
.L7:
        call    rax
        mov     rax, QWORD PTR [rbx]
        mov     rax, QWORD PTR [rax]
        cmp     rax, OFFSET FLAT:Derived::pr_fn()
        je      .L11
.L9:
        mov     rdi, rbx
        pop     rbx
        jmp     rax
main:
        sub     rsp, 8
        mov     edx, 5
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edx, 8
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        xor     eax, eax
        add     rsp, 8
        ret

See code here

Observations:

  1. No assembly is generated for Base::pr_fn2(), Derived::pr_fn() and Derived::pr_fn3() in Code1.
  2. All the above mentioned functions are inlined in the assembly of main() in Code1.
  3. Assembly is generated for Interface::pr_fn3(), Base::pr_fn2(), Derived::pr_fn() and Derived::pr_fn3() in Code2.
  4. All the above mentioned functions are inlined in the assembly of main() in Code2.
  5. The calls to Interface::pr_fn3(), Base::pr_fn2() and Derived::pr_fn() are inlined in the assembly of Derived::pr_fn3() in Code2.

My question: When the program is run and main function is invoked in both cases same assembly instructions are performed. Then why the compiler generates the assembly for other functions in Code2 but not in Code1 ?

Update 1: (Interface::pr_fn3() is implemented inside class definition)

  1. As @PeterCordes mentioned - "Code2's void Interface::pr_fn3(){ ... } defined outside the struct{} is not implicitly inline." When its definition is moved inside the struct its definition is not emitted in the assembly. Yet definitions for Base::pr_fn2(), Derived::pr_fn(), Derived::pr_fn3() are emitted in Code2 but not in Code1. Why is this difference occurring? See here
  2. Compiling and linking the code (from above point) to binary removes the definitions of Base::pr_fn2() and Derived::pr_fn() in assembly. But why Derived::pr_fn3() is still emitted? See here
  3. Clang 19.1.0 generates same assembly for both cases. Then why gcc behaves differently? See here

Note: Please correct me if my understanding is wrong anywhere.

I experimented with some virtual methods and observed the assembly. I expected to see no difference in assembly with -O3 optimization, but found some unnecessary assembly is generated. I googled but no luck. So I am asking the question here.

Upvotes: 5

Views: 115

Answers (0)

Related Questions