Недавно я смотрел в коде проекта с открытым исходным кодом, и я увидел кучу операторов формы T & object = *dynamic_cast<T*>(ptr);
.
(На самом деле это происходило в макросе, используемом для объявления многих функций по аналогичной схеме.)
Для меня это выглядело как кодовый запах. Мое рассуждение состояло в том, что если вы знаете, что у вас получится, то почему бы не использовать static_cast
? Если вы не уверены, не следует ли использовать тест для тестирования? Поскольку компилятор может предположить, что любой указатель, что вы *
не является нулевым.
Я спросил одного из разработчиков на irc об этом, и он сказал, что он считает, что static_cast
downcast небезопасен. Они могли бы добавить утверждение, но даже если это не так, он говорит, что вы все равно получите разыменование и сбой нулевого указателя, когда фактически используется obj
. (Поскольку при ошибке dynamic_cast
преобразует указатель в нуль, тогда, когда вы обращаетесь к любому члену, вы будете считывать с некоторого адреса значения, очень близкого к нулю, что ОС не позволит.) Если вы используете a static_cast
, и это плохо, вы можете просто получить некоторое повреждение памяти. Таким образом, используя опцию *dynamic_cast
, вы торгуете со скоростью для немного лучшей отладки. Вы не платите за утверждение, вместо этого вы в основном полагаетесь на ОС, чтобы поймать разворот nullptr, по крайней мере, то, что я понял.
Я принял это объяснение в то время, но это беспокоило меня, и я подумал об этом еще немного.
Вот мои рассуждения.
Если я понимаю стандартное право, приведенный указатель static_cast
в основном означает выполнение некоторой фиксированной арифметики указателей. То есть, если у меня есть A * a
, и я статирую его связанным типом B *
, то что компилятор на самом деле собирается сделать с этим, это добавить смещение к указателю, смещение зависит только от макета типы A
, B
, (и какая реализация С++ потенциально). Эту теорию можно проверить с помощью статических указателей на void *
и вывести их до и после статического броска. Я ожидаю, что если вы посмотрите на сгенерированную сборку, static_cast
превратится в "добавить некоторую фиксированную константу в регистр, соответствующий указателю".
A dynamic_cast
средство для перевода указателя, сначала проверьте RTTI и выполняйте статическую трансляцию, если она действительна на основе динамического типа. Если это не так, верните nullptr
. Поэтому я ожидаю, что компилятор в какой-то момент расширит выражение dynamic_cast<B*>(ptr)
, где ptr
имеет тип A*
в выражение типа
(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr)
Однако, если мы тогда *
, результат dynamic_cast, *
of nullptr
равен UB, поэтому мы неявно обещаем, что ветвь nullptr
никогда не произойдет. И соответствующим компиляторам разрешено "отступать" от этого и исключать "нулевые проверки", находящуюся в центре внимания в Chris Lattner знаменитый пост в блоге.
Если тестовая функция __validate_dynamic_cast_A_to_B(ptr)
непрозрачна для оптимизатора, то есть она может иметь побочные эффекты, то оптимизатор не может избавиться от нее, даже если она "знает" ветку nullptr не произойдет. Однако, вероятно, эта функция не является непрозрачной для оптимизатора - возможно, она очень хорошо понимает возможные побочные эффекты.
Итак, я ожидаю, что оптимизатор, по существу, преобразует *dynamic_cast<T*>(ptr)
в *static_cast<T*>(ptr)
, и что их перестановка должна дать ту же сгенерированную сборку.
Если true, это оправдывало бы мой первоначальный аргумент, что *dynamic_cast<T*>
- это запах кода, даже если вы действительно не заботитесь о UB в своем коде и только заботитесь о том, что происходит на самом деле. Поскольку, если соответствующему компилятору будет разрешено без необходимости изменять его на static_cast
, тогда вы не получаете никакой безопасности, как вы думаете, поэтому вы должны явно указать t21 или явно утверждать. По крайней мере, это будет мой голос в обзоре кода. Я пытаюсь выяснить, действительно ли этот аргумент прав.
Вот что говорится в стандарте dynamic_cast
:
[5.2.7]
Динамический ролик[expr.dynamic.cast]
1. Результат выраженияdynamic_cast<T>(v)
является результатом преобразования выраженияv
в типT
.T
должен быть указателем или ссылкой на полный тип класса или "указатель на cv void". Операторdynamic_cast
не должен отбрасывать константу.
...
8. ЕслиC
- тип класса, к которому указывает или ссылаетсяT
, проверка выполнения выполняется логически следующим образом: (8.1). Если в самом производном объекте, указанном (указанном) наv
,v
указывает (относится) к подобъекту общедоступного базового класса объектаC
, и если только один объект типаC
получается из подобъекта, на который указывает (ссылается) наv
результат (относится) к этому объектуC
.
(8.2) - В противном случае, еслиv
указывает (относится) к подобъекту открытого базового класса самого производного объекта, а тип самого производного объекта имеет базовый класс типаC
, который является однозначным и общедоступным, результирующие точки (ссылаются) на подобъектC
самого производного объекта.
(8.3) - В противном случае проверка времени выполнения не выполняется.
Предполагая, что иерархия классов известна во время компиляции, также известны относительные смещения каждого из этих классов в макетах eachothers. Если v
является указателем на тип A
, и мы хотим отнести его к указателю типа B
, и приведение недвусмысленно, тогда сдвиг, который должен принимать v
, является константой времени компиляции. Даже если v
фактически указывает на объект более производного типа C
, этот факт не меняется, где подобъект A
лежит относительно подобъекта B
, правильно? Так что независимо от типа C
, даже если это неизвестный тип из другой единицы компиляции, насколько мне известно, результат dynamic_cast<T*>(ptr)
имеет только два возможных значения: nullptr
или "фиксированное смещение от ptr
> ".
Тем не менее, сюжет немного сгущается, фактически просматривая какой-то код gen.
Вот простая программа, которую я сделал, чтобы исследовать это:
int output = 0;
struct A {
explicit A(int n) : num_(n) {}
int num_;
virtual void foo() {
output += num_;
}
};
struct B final : public A {
explicit B(int n) : A(n), num2_(2 * n) {}
int num2_;
virtual void foo() override {
output -= num2_;
}
};
void visit(A * ptr) {
B & b = *dynamic_cast<B*>(ptr);
b.foo();
b.foo();
}
int main() {
A * ptr = new B(5);
visit(ptr);
ptr = new A(10);
visit(ptr);
return output;
}
В соответствии с проводником компилятора godbolt, сборка gcc 5.3
x86 для этого с параметрами -O3 -std=c++11
выглядит следующим образом:
A::foo():
movl 8(%rdi), %eax
addl %eax, output(%rip)
ret
B::foo():
movl 12(%rdi), %eax
subl %eax, output(%rip)
ret
visit(A*):
testq %rdi, %rdi
je .L4
subq $8, %rsp
xorl %ecx, %ecx
movl typeinfo for B, %edx
movl typeinfo for A, %esi
call __dynamic_cast
movl 12(%rax), %eax
addl %eax, %eax
subl %eax, output(%rip)
addq $8, %rsp
ret
.L4:
movl 12, %eax
ud2
main:
subq $8, %rsp
movl $16, %edi
call operator new(unsigned long)
movq %rax, %rdi
movl $5, 8(%rax)
movq vtable for B+16, (%rax)
movl $10, 12(%rax)
call visit(A*)
movl $16, %edi
call operator new(unsigned long)
movq vtable for A+16, (%rax)
movl $10, 8(%rax)
movq %rax, %rdi
call visit(A*)
movl output(%rip), %eax
addq $8, %rsp
ret
typeinfo name for A:
typeinfo for A:
typeinfo name for B:
typeinfo for B:
vtable for A:
vtable for B:
output:
.zero 4
Когда я изменяю dynamic_cast
на a static_cast
, вместо этого получаю следующее:
A::foo():
movl 8(%rdi), %eax
addl %eax, output(%rip)
ret
B::foo():
movl 12(%rdi), %eax
subl %eax, output(%rip)
ret
visit(A*):
movl 12(%rdi), %eax
addl %eax, %eax
subl %eax, output(%rip)
ret
main:
subq $8, %rsp
movl $16, %edi
call operator new(unsigned long)
movl $16, %edi
subl $20, output(%rip)
call operator new(unsigned long)
movl 12(%rax), %edx
movl output(%rip), %eax
subl %edx, %eax
subl %edx, %eax
movl %eax, output(%rip)
addq $8, %rsp
ret
output:
.zero 4
Здесь то же самое с clang 3.8
и теми же параметрами.
dynamic_cast
:
visit(A*): # @visit(A*)
xorl %eax, %eax
testq %rdi, %rdi
je .LBB0_2
pushq %rax
movl typeinfo for A, %esi
movl typeinfo for B, %edx
xorl %ecx, %ecx
callq __dynamic_cast
addq $8, %rsp
.LBB0_2:
movl output(%rip), %ecx
subl 12(%rax), %ecx
movl %ecx, output(%rip)
subl 12(%rax), %ecx
movl %ecx, output(%rip)
retq
B::foo(): # @B::foo()
movl 12(%rdi), %eax
subl %eax, output(%rip)
retq
main: # @main
pushq %rbx
movl $16, %edi
callq operator new(unsigned long)
movl $5, 8(%rax)
movq vtable for B+16, (%rax)
movl $10, 12(%rax)
movl typeinfo for A, %esi
movl typeinfo for B, %edx
xorl %ecx, %ecx
movq %rax, %rdi
callq __dynamic_cast
movl output(%rip), %ebx
subl 12(%rax), %ebx
movl %ebx, output(%rip)
subl 12(%rax), %ebx
movl %ebx, output(%rip)
movl $16, %edi
callq operator new(unsigned long)
movq vtable for A+16, (%rax)
movl $10, 8(%rax)
movl typeinfo for A, %esi
movl typeinfo for B, %edx
xorl %ecx, %ecx
movq %rax, %rdi
callq __dynamic_cast
subl 12(%rax), %ebx
movl %ebx, output(%rip)
subl 12(%rax), %ebx
movl %ebx, output(%rip)
movl %ebx, %eax
popq %rbx
retq
A::foo(): # @A::foo()
movl 8(%rdi), %eax
addl %eax, output(%rip)
retq
output:
.long 0 # 0x0
typeinfo name for A:
typeinfo for A:
typeinfo name for B:
typeinfo for B:
vtable for B:
vtable for A:
static_cast
:
visit(A*): # @visit(A*)
movl output(%rip), %eax
subl 12(%rdi), %eax
movl %eax, output(%rip)
subl 12(%rdi), %eax
movl %eax, output(%rip)
retq
main: # @main
retq
output:
.long 0 # 0x0
Итак, в обоих случаях, кажется, что dynamic_cast
не может быть устранено оптимизатором:
Кажется, он вызывает вызовы таинственной функции __dynamic_cast
, используя типinfo обоих классов, независимо от того, что. Даже если все оптимизации включены, а B
отмечен как final.
-
Есть ли у этого низкоуровневого вызова побочные эффекты, которые я не рассматривал? Я понял, что vtables по существу фиксированы и что vptr в объекте не меняется... я прав? У меня есть только базовое знакомство с тем, как фактически реализуются виртуальные потоки, и я обычно избегаю виртуальных функций в своем коде, поэтому я не думал об этом глубоко или накопленный опыт.
-
Я прав, что соответствующий компилятор может заменить
*dynamic_cast<T*>(ptr)
на*static_cast<T*>(ptr)
как правильную оптимизацию? -
Верно ли это, что "обычно" (что означает, что на машинах x86, скажем так, и кастинг между классами в иерархии "обычной" сложности) a
dynamic_cast
не может быть оптимизирован и фактически будет производить anullptr
, даже если вы*
это сразу после, приводя кnullptr
разыменованию и сбою при доступе к объекту? -
Является ли "всегда заменить
*dynamic_cast<T*>(ptr)
либо тестомdynamic_cast
+, либо каким-либо утверждением, либо с помощью*static_cast<T*>(ptr)
" звукового совета?