#опытным
Концепция бесплатных абстракций-то есть. Но концепции они обычно в банках-консервах хранятся. В реальности все немного сложнее и бесплатный только труд стажеров в современном IT.
Вот как много из вас думает, что уникальный указатель - это бесплатная абстракция? Думаю, что очень многие. Но давайте посмотрим на реальность.
Для чего используется уникальный указатель? Чтобы избавиться от сырых указателей, снять головную боль по освобождению памяти и четко показать в коде передачу владения. То есть такой код:
void bar(int *ptr);
// Takes ownership
void baz(int *ptr);
void foo(int *ptr) {
if (*ptr > 42) {
bar(ptr);
*ptr = 42;
}
baz(ptr);
}
хорошо бы переписать с использованием std::unique_ptr:
void bar(int *ptr);
// Takes ownership.
void baz(std::unique_ptr<int> ptr);
void foo(std::unique_ptr<int> ptr) {
if (*ptr > 42) {
bar(ptr.get());
*ptr = 42;
}
baz(std::move(ptr));
}
Будет ли здесь оверхэд рантайма? Ох-ох-ох, еще как!
Вот асм для первого сниппета:
foo(int):
cmpl $43, (%rdi)
jl baz(int)@PLT
pushq %rbx
movq %rdi, %rbx
callq bar(int)@PLT
movq %rbx, %rdi
movl $42, (%rbx)
popq %rbx
jmp baz(int)@PLT
А вот для второго:
foo(std::unique_ptr<int, std::default_delete<int>>):
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq (%rdi), %rdi
cmpl $43, (%rdi)
jl .LBB0_2
callq bar(int)@PLT
movq (%rbx), %rdi
movl $42, (%rdi)
.LBB0_2:
movq %rdi, 8(%rsp)
movq $0, (%rbx)
leaq 8(%rsp), %rdi
callq baz(std::unique_ptr<int, std::default_delete<int>>)@PLT
movq 8(%rsp), %rdi
testq %rdi, %rdi
je .LBB0_5
movl $4, %esi
callq operator delete(void, unsigned long)@PLT
.LBB0_5:
addq $16, %rsp
popq %rbx
retq
movq %rax, %rbx
movq 8(%rsp), %rdi
testq %rdi, %rdi
je .LBB0_8
movl $4, %esi
callq operator delete(void, unsigned long)@PLT
.LBB0_8:
movq %rbx, %rdi
callq _Unwind_Resume@PLT
Он в 4 раза больше! Если внимательно присмотреться, то можно увидеть, что значительная часть кода здесь тратится на подчищение памяти и обработку исключений. Да, исключения - это вещь, где RAII очень сильно помогает. Вот вам ссылочки на годболт: тык и тык
Но предположим, что изначальный код был корректным и там не вылетало никаких исключений. Пометим функции как noexcept:
void bar(int *ptr) noexcept;
void baz(std::unique_ptr<int> ptr) noexcept;
void foo(std::unique_ptr<int> ptr) {
if (*ptr > 42) {
bar(ptr.get());
*ptr = 42;
}
baz(std::move(ptr));
}
Вот асм:
foo(std::unique_ptr<int, std::default_delete<int>>):
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq (%rdi), %rdi
cmpl $43, (%rdi)
jl .LBB0_2
callq bar(int)@PLT
movq (%rbx), %rdi
movl $42, (%rdi)
.LBB0_2:
movq %rdi, 8(%rsp)
movq $0, (%rbx)
leaq 8(%rsp), %rdi
callq baz(std::unique_ptr<int, std::default_delete<int>>)@PLT
movq 8(%rsp), %rdi
testq %rdi, %rdi
je .LBB0_4
movl $4, %esi
callq operator delete(void, unsigned long)@PLT
.LBB0_4:
addq $16, %rsp
popq %rbx
retq
Стало чуть меньше, но это явно не zero-cost.
Любую проблему можно решить с помощью введения дополнительного уровня индирекции, но эти индирекции накладывают свои косты.
Этот пост не для того, чтобы не пользоваться абстракциями. Просто у всего есть цена и надо это осознавать и в жизни, и при написании программ.
Be aware about costs. Stay cool.