I'm wondering if the emplace_back
and push_back
methods of std::vector
behave any different when using primitive scalar types, such as std::uint32_t
or std::uint8_t
. Intuitively, I would guess that, after compilation, both variants would lead to the same bytecode here:
void copyListContent(std::uint8_t * list, std::size_t nElems,
std::vector<std::uint8_t> & vec)
{
vec.clear();
vec.reserve(nElems);
for (std::size_t i = 0; i < nElems; ++i)
{
//variant 1:
vec.push_back(list[i]);
//variant 2:
vec.emplace_back(list[i]);
}
}
Please correct me if that should be already wrong...
Now, where I'm starting to struggle is when I'm asking myself what happens if the types of the "list" and vector don't match:
void copyListContent(std::uint8_t * list, std::size_t nElems,
std::vector<std::uint32_t> & vec)
{
//... same code as above
}
The std::uint8_t
elements will be converted to std::uint32_t
when putting them into the vector (using emplace_back
or push_back
), so I'm wondering if that triggers some "constructor" to be called? In that case, would emplace_back
be more efficient, because it would avoid to construct a temporary object that would be copied? Or are these implicit conversions that don't make any difference, and emplace_back
and push_back
will behave the same?
So, I'm asking myself, and you:
For primitive types like these, do emplace_back
and push_back
always behave similarly?
As a vague guess I'd say "probably yes", but I have not enough knowledge about C++ internals to reliably answer this for myself. I'd be happy to learn how things work in this case - thanks a lot in advance!
GCC compiles both versions of the code to the same resulting assembly (Godbolt.org):
#include<vector>
void push(std::vector<int> & vec, int val) {
vec.push_back(val);
}
vs
#include<vector>
void push(std::vector<int> & vec, int val) {
vec.emplace_back(val);
}
Both result in the following assembly:
push(std::vector<int, std::allocator<int> >&, int):
push r15
push r14
push r13
push r12
push rbp
push rbx
sub rsp, 24
mov rbx, QWORD PTR [rdi+8]
cmp rbx, QWORD PTR [rdi+16]
je .L2
mov DWORD PTR [rbx], esi
add rbx, 4
mov QWORD PTR [rdi+8], rbx
add rsp, 24
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
.L2:
mov r12, QWORD PTR [rdi]
mov r14, rbx
mov ecx, esi
mov rbp, rdi
sub r14, r12
mov rax, r14
sar rax, 2
je .L9
lea rdx, [rax+rax]
mov r15, -4
cmp rax, rdx
ja .L4
movabs rsi, 4611686018427387903
cmp rdx, rsi
jbe .L19
.L4:
mov rdi, r15
mov DWORD PTR [rsp], ecx
call operator new(unsigned long)
mov ecx, DWORD PTR [rsp]
mov r13, rax
add r15, rax
.L5:
lea rax, [r13+4+r14]
mov DWORD PTR [r13+0+r14], ecx
mov QWORD PTR [rsp], rax
cmp rbx, r12
je .L6
mov rdx, r14
mov rsi, r12
mov rdi, r13
call memmove
.L7:
mov rdi, r12
call operator delete(void*)
.L8:
mov QWORD PTR [rsp+8], r13
movq xmm0, QWORD PTR [rsp+8]
mov QWORD PTR [rbp+16], r15
movhps xmm0, QWORD PTR [rsp]
movups XMMWORD PTR [rbp+0], xmm0
add rsp, 24
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
.L6:
test r12, r12
je .L8
jmp .L7
.L9:
mov r15d, 4
jmp .L4
.L19:
xor r15d, r15d
xor r13d, r13d
test rdx, rdx
je .L5
lea r15, [0+rax*8]
jmp .L4
As you might have deduced, this is not behavior you can depend on when dealing with types that have more complex constructing/copying/moving behaviors, but for primitive types, the difference is negligible.
Having said that, there's one situation where there might be a difference:
std::vector<int16_t> vec;
size_t seed = 0x123456789abcdef;
vec.push_back(seed);
vs
vec.emplace_back(seed);
In a (properly optimized) compiler, the two assembly codes will probably be identical, but you'll get different narrowing warnings (or errors, if you force warnings to cause compilation failure) from the compiler. The latter is more likely to give a difficult-to-diagnose warning message, as the error will originate from inside <vector>
instead of inside whatever .cpp file made the call.
The Google guidelines as published on the Abseil site: https://abseil.io/tips/112 are that you should prefer to use push_back as it is more readable.
Worrying about the implicit conversions for built in types seems like premature optimisation; chances are your compiler will optimise the conversion away anyway.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With