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
Observations:
- No assembly is generated for
Base::pr_fn2(),Derived::pr_fn()andDerived::pr_fn3()in Code1. - All the above mentioned functions are inlined in the assembly of
main()in Code1. - Assembly is generated for
Interface::pr_fn3(),Base::pr_fn2(),Derived::pr_fn()andDerived::pr_fn3()in Code2. - All the above mentioned functions are inlined in the assembly of
main()in Code2. - The calls to
Interface::pr_fn3(),Base::pr_fn2()andDerived::pr_fn()are inlined in the assembly ofDerived::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)
- As @PeterCordes mentioned - "Code2's void
Interface::pr_fn3(){ ... }defined outside thestruct{}is not implicitly inline." When its definition is moved inside the struct its definition is not emitted in the assembly. Yet definitions forBase::pr_fn2(),Derived::pr_fn(),Derived::pr_fn3()are emitted in Code2 but not in Code1. Why is this difference occurring? See here - Compiling and linking the code (from above point) to binary removes the definitions of
Base::pr_fn2()andDerived::pr_fn()in assembly. But whyDerived::pr_fn3()is still emitted? See here - 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.
void Interface::pr_fn3(){ ... }defined outside thestruct{}is not implicitlyinline. In Code1, all the member functions are defined inline and thus are implicitlyinline, so no stand-alone definition is needed in a compilation unit that inlines all the calls. It's not immediately obvious to me why stand-alone definitions forDerived::pr_fn()and so on are also emittedInterface::pr_fn3()gets emitted in case a call from another compilation-unit reachesInterface::pr_fn3(). Definitions have to exist for the vtable to point to them. C++-capable linkers will discard duplicate definitions.Interface::pr_fn3()itselfinlineso we don't see a definition for it. But other compilation units could exist that only see thestruct Interface{...}declaration, not the definitions forBaseorDerived, but vtable entries have to point somewhere for the virtual method calls in the version ofInterface::pr_fn3()that could run in that other compilation unit. So I guess GCC chooses to emit them from this file. No references to aBaseorDerivedobject escape this compilation unit, but I guess that's not something GCC tries to detect to avoid emitting defs?