Fatiando em C++ onde estou errado?
-
21-12-2019 - |
Pergunta
Eu li sobre o problema de fatiamento em C++ e tentei alguns exemplos (tenho experiência em Java).Infelizmente, não entendo parte do comportamento.Atualmente, estou preso neste exemplo (exemplo alternativo do Efficent C++ Third Edition).Alguém pode me ajudar a entender isso?
Minha classe simples de pai:
class Parent
{
public:
Parent(int type) { _type = type; }
virtual std::string getName() { return "Parent"; }
int getType() { return _type; }
private:
int _type;
};
Minha classe simples de uma criança:
class Child : public Parent
{
public:
Child(void) : Parent(2) {};
virtual std::string getName() { return "Child"; }
std::string extraString() { return "Child extra string"; }
};
O principal:
void printNames(Parent p)
{
std::cout << "Name: " << p.getName() << std::endl;
if (p.getType() == 2)
{
Child & c = static_cast<Child&>(p);
std::cout << "Extra: " << c.extraString() << std::endl;
std::cout << "Name after cast: " << c.getName() << std::endl;
}
}
int main()
{
Parent p(1);
Child c;
printNames(p);
printNames(c);
}
Após a execução, recebo:
Nome:Pai
Nome:Pai
Extra:Corda extra filho
Nome após elenco:Pai
Entendo as duas primeiras linhas, porque é a causa do "fatiamento".Mas não entendo por que posso transformar o filho em pai por meio da conversão estática.No livro está escrito que após o fatiamento, todas as informações especializadas serão cortadas.Então, suponho que não posso lançar p em c, porque não tenho informações (no início da função printNames é criado um novo objeto Parent sem as informações adicionais).
Além disso, por que estou recebendo o "Nome após elenco:Pai" se a conversão foi bem-sucedida em vez de "Nome após conversão:Criança"?
Solução
Seu resultado é um extraordinário golpe de azar.Este é o resultado que estou obtendo:
Name: Parent
Name: Parent
Extra: Child extra string
bash: line 8: 6391 Segmentation fault (core dumped) ./a.out
Aqui está o que está acontecendo no seu código:
Quando você passa c
para printNames
, a conversão acontece em.Em particular, como a passagem é por valor, você invoca o construtor de cópia de Parent
, que é declarado implicitamente e cujo código ficaria assim:
Parent(Parent const& other) : _type{other._type} {}
Em outras palavras, você copia o _type
variável de c
e nada mais.Você agora tem um novo objeto do tipo Parent
(tanto seu tipo estático quanto dinâmico são Parent
) e c
não é realmente passado para printNames
de forma alguma.
Dentro da função, você converte à força p
para um Child&
.Este elenco não pode ter sucesso porque p
simplesmente não é um Child
, ou conversível para um, mas C++ não fornece nenhum diagnóstico para isso (o que é realmente uma pena, já que o compilador poderia provar trivialmente que a conversão está errada).
Agora estamos na terra do comportamento indefinido e agora tudo pode acontecer.Na prática, desde Child::extraString
nunca acessa this
(implícita ou explicitamente), a chamada para essa função é bem-sucedida.A chamada é feita em um objeto ilegal, mas como o objeto nunca é tocado, isso funciona (mas ainda é ilegal!).
A próxima chamada, para Child::getName
, é uma chamada virtual e, portanto, precisa acessar explicitamente this
(na maioria das implementações, ele acessa o ponteiro da tabela de métodos virtuais).E novamente, como o código é UB de qualquer maneira, tudo pode acontecer.Você teve “sorte” e o código simplesmente pegou o ponteiro da tabela de métodos virtuais da classe pai.Com meu compilador, esse acesso obviamente falhou.
Outras dicas
Esse código é horrível.O que está acontecendo:
printNames(c)
fatiasc
, construção de cópia localp
deParent
objeto incorporado no chamadorc
objeto e, em seguida, definirp
ponteiro para oParent
tabela de despacho virtual.porque os membros de dados de
Parent
foram copiados dec
, o tipo dep
é 2 e oif
filial é inseridaChild & c = static_cast<Child&>(p);
efetivamente diz ao compilador "confie em mim (sou um programador) e sei dissop
é na verdade umChild
objeto ao qual eu gostaria de fazer referência", mas isso é uma mentira descarada, poisp
é na verdade umParent
objeto copiado doChild
c
- a responsabilidade recai sobre você, como programador, de não pedir ao compilador para fazer isso se não tiver certeza de que é válido
c.extraString()
é encontrado estaticamente (em tempo de compilação) pelo compilador porque ele sabec
é umChild
(ou outro tipo derivado, masc.extraString
não évirtual
então pode ser resolvido estaticamente;isso é comportamento indefinido fazer isso em umParent
objeto, mas provavelmente porqueextraString
não tenta acessar nenhum dado que apenas umChild
objeto teria, ele aparentemente funcionou "ok" para vocêc.getName()
évirtual
, então o compilador usa a tabela de despacho virtual do objeto - porque o objeto é realmente umParent
ele resolve dinamicamente (em tempo de execução) para oParent::getName
função e produz a saída associada- a implementação do despacho virtual é definida pela implementação, e seu comportamento indefinido pode não funcionar dessa maneira em todas as implementações de C++, ou mesmo em todos os níveis de otimização, com todas as opções do compilador, etc.