Desafio C++

Instrumentos musicais com final

Enunciado

Crie uma hierarquia de instrumentos musicais. A classe base abstrata Instrumento deve ter um método virtual puro tocar() const e um destrutor virtual. As classes Corda e Sopro herdam de Instrumento e implementam tocar(). A classe Violao herda de Corda e sobrescreve tocar(). A classe Flauta herda de Sopro e é marcada como final, impedindo herança adicional. No main, crie um vetor de std::unique_ptr<Instrumento>, adicione um Violao e uma Flauta, e chame tocar() para cada um. Além disso, tente (comentado) criar uma classe que herde de Flauta para ver o erro de compilação.

Requisitos

  • Instrumento é classe abstrata com virtual void tocar() const = 0 e destrutor virtual.
  • Corda e Sopro herdam publicamente de Instrumento e implementam tocar() com override.
  • Violao herda de Corda e sobrescreve tocar() com override.
  • Flauta herda de Sopro e é marcada como final.
  • O main usa std::unique_ptr<Instrumento> e chama tocar() polimorficamente.
  • Inclua um comentário mostrando a tentativa de herdar de Flauta e explique o erro.

Código inicial

#include <iostream>
#include <memory>
#include <vector>

// TODO: definir classes Instrumento, Corda, Sopro, Violao, Flauta

int main() {
    // TODO: criar vetor de unique_ptr<Instrumento> e chamar tocar()
    return 0;
}

Saída esperada

Violao: acorde harmonioso
Flauta: som suave
Ver dica

Use = 0 para tornar tocar() virtual pura. A palavra-chave final vai após o nome da classe: class Flauta final : public Sopro.

Mostrar solução
#include <iostream>
#include <memory>
#include <vector>

class Instrumento {
public:
    virtual void tocar() const = 0; // método virtual puro
    virtual ~Instrumento() = default; // destrutor virtual
};

class Corda : public Instrumento {
public:
    void tocar() const override {
        std::cout << "Corda: som dedilhado\n";
    }
};

class Sopro : public Instrumento {
public:
    void tocar() const override {
        std::cout << "Sopro: som soprado\n";
    }
};

class Violao : public Corda {
public:
    void tocar() const override {
        std::cout << "Violao: acorde harmonioso\n";
    }
};

class Flauta final : public Sopro {
public:
    void tocar() const override {
        std::cout << "Flauta: som suave\n";
    }
};

// Tentativa de herdar de Flauta (descomente para ver o erro):
// class FlautaDoce : public Flauta { };
// Erro: cannot derive from 'final' base 'Flauta'

int main() {
    std::vector<std::unique_ptr<Instrumento>> instrumentos;
    instrumentos.push_back(std::make_unique<Violao>());
    instrumentos.push_back(std::make_unique<Flauta>());

    for (const auto& i : instrumentos) {
        i->tocar();
    }
    return 0;
}

Passo a passo

  1. A classe Instrumento é abstrata porque contém o método virtual puro virtual void tocar() const = 0;. Isso significa que não podemos instanciar Instrumento diretamente.
  2. Incluímos um destrutor virtual virtual ~Instrumento() = default; para garantir que, ao deletar um objeto via ponteiro para Instrumento, o destrutor correto da classe derivada seja chamado.
  3. A classe Corda herda publicamente de Instrumento e implementa tocar() com override, imprimindo uma mensagem genérica de corda.
  4. A classe Sopro também herda publicamente e implementa tocar() com override, imprimindo uma mensagem genérica de sopro.
  5. A classe Violao herda de Corda e sobrescreve tocar() com override, fornecendo uma implementação específica.
  6. A classe Flauta herda de Sopro e é marcada como final. Isso impede que outras classes herdem de Flauta. Seu método tocar() é sobrescrito.
  7. No main, criamos um std::vector<std::unique_ptr<Instrumento>> e adicionamos um Violao e uma Flauta usando std::make_unique.
  8. Iteramos sobre o vetor e chamamos tocar(). Devido ao polimorfismo, as implementações corretas são chamadas.
  9. O código comentado mostra a tentativa de herdar de Flauta. Se descomentado, o compilador emitirá um erro porque Flauta é final.

Por que funciona

A classe abstrata Instrumento define a interface que todas as classes derivadas devem seguir. O método virtual puro força as subclasses a implementarem tocar(). O polimorfismo permite tratar objetos de diferentes classes derivadas através de ponteiros para a base. O destrutor virtual garante a destruição correta. A palavra-chave final impede herança adicional, o que pode ser útil para evitar modificações indesejadas ou para permitir otimizações.

Erros comuns

  • Esquecer = 0 no método virtual puro: Se tocar() não for puro, Instrumento não será abstrata e poderá ser instanciada, mas não é o desejado. Use virtual void tocar() const = 0;.
  • Não usar override: Se a assinatura não coincidir (por exemplo, faltar const), o método não sobrescreve e o compilador pode não avisar. Use override para que o compilador verifique.
  • Destrutor não virtual: Se ~Instrumento() não for virtual, ao deletar um Violao via unique_ptr<Instrumento>, apenas o destrutor de Instrumento seria chamado, causando vazamento de recursos. Declare virtual ~Instrumento() = default;.
  • Tentar herdar de classe final: Se você tentar class FlautaDoce : public Flauta { }, o compilador emitirá um erro. Remova final se precisar herdar.

Outra forma de resolver

Poderia-se usar composição em vez de herança, mas a herança é adequada para modelar a relação "é um" entre instrumentos. Outra alternativa é usar std::variant ou std::function para armazenar callables, mas isso foge do conceito de herança e polimorfismo.

Saída esperada

Violao: acorde harmonioso
Flauta: som suave