Se però vuoi mantenere l'informazione del tipo iniziale puoi usare il metodo "classico" C per gestire queste situazioni: struct+union+enum, condendola con un goccio di C++ per renderla più comoda:
codice:
#include <stack>
#include <iostream>
#include <stdexcept>

using std::cout;

// L'elemento da mettere nella lista
struct Elem
{
    // Una union condivide la memoria tra due elementi
    // Di fatto significa che puoi leggere solo dall'ultimo che hai scritto se vuoi avere risultati sensati
    // Qui memorizziamo i dati
    union
    {
        char c;
        int i;
    } u;
    // Qui invece memorizziamo il tipo effettivamente registrato in u
    enum
    {
        elemChar,
        elemInt
    } type;
    
    // Costruttori per rendere comodo l'uso della struct
    Elem(char c) : type(elemChar)
    {
        u.c=c;
    }

    Elem(int i) : type(elemInt)
    {
        u.i=i;
    }

    // Operatori di conversione implicita ai tipi desiderati
    operator int() const
    {
        if(type!=elemInt)
            throw std::logic_error("Questo elemento non contiene un int.");
        return u.i;
    }

    operator char() const
    {
        if(type!=elemChar)
            throw std::logic_error("Questo elemento non contiene un char.");
        return u.i;
    }
};

// Operatore di scrittura su stream per comodità
std::ostream & operator<<(std::ostream & os, const Elem & right)
{
     if(right.type==Elem::elemInt)
        os<<(int)right;
    else
        os<<(char)right;
}

int main()
{
    std::stack<Elem> stack;
    stack.push('a');
    stack.push(1);
    int i = stack.top(); // memorizza 1 in i (estrae l'int grazie al cast implicito - per sicurezza può essere meglio usare un cast esplicito in casi ambigui)
    cout<<"Ho estratto: "<<i<<"\n";
    stack.pop();
    cout<<"Ora scrivo l'altro elemento direttamente sullo stream: "<<stack.top()<<"\n"; // stampa a grazie all'operatore in overload
    try
    {
        // Prova a prendere un char e un int da un elemento che in realtà contiene un char
        char c = stack.top();
        cout<<"char estratto senza problemi!\n";
        int i = stack.top();
        cout<<"int estratto senza problemi!\n";
    }
    catch(std::exception & ex)
    {
        cout<<"Catturata eccezione: "<<ex.what()<<"\n";
    }
    return 0;
}
(lo vedi in azione direttamente qui)

L'alternativa, per non stare a reinventare la ruota ogni volta, può essere usare boost::variant (che va bene per casi come questo in cui i tipi da memorizzare sono determinati in maniera fissa)
codice:
#include <stack>
#include <iostream>
#include <boost/variant.hpp>
 
using std::cout;
 
int main()
{
    std::stack<boost::variant<char, int> > stack;
    stack.push('a');
    stack.push(1);
    int i = boost::get<int>(stack.top()); // memorizza 1 in i
    cout<<"Ho estratto: "<<i<<"\n";
    stack.pop();
    cout<<"Ora scrivo l'altro elemento direttamente sullo stream: "<<stack.top()<<"\n"; // stampa a grazie all'operatore in overload
    try
    {
        // Prova a prendere un char e un int da un elemento che in realtà contiene un char
        char c = boost::get<char>(stack.top());
        cout<<"char estratto senza problemi!\n";
        int i = boost::get<int>(stack.top());
        cout<<"int estratto senza problemi!\n";
    }
    catch(std::exception & ex)
    {
        cout<<"Catturata eccezione: "<<ex.what()<<"\n";
    }
    return 0;
}
(link)

Vale la pena di sapere che esiste anche boost::any se devi poter memorizzare qualunque valore.