I try to use template to achieve this, but it doesn't work.
I define an Incomplete template class in the internal header file test_inner.hpp:
#define VISIBLE __attribute__((visibility("default")))
typedef int Dummy;
template <typename T> class Incomplete
{
};
And in the src file, I specialized the Incomplete<Dummy>:
#include "test_inner.hpp"
template <> class VISIBLE Incomplete<Dummy>
{
    private:
    int a = 3;
    public:
    int f()
    {
        std::cout << "a: " << a << std::endl;
        return 0;
    }
};
template class Incomplete<Dummy>;
extern "C" VISIBLE
void test(Incomplete<Dummy> *a)
{
    a->f();
}
In the external header file, just declare the explicit instance of Incomplete:
#include "test_inner.hpp"
extern template class VISIBLE Incomplete<Dummy>;
extern "C" VISIBLE void test(Incomplete<Dummy> *a);
The above code will be built into a shared library, and the following is my test code:
#include "test.hpp"
test(new Incomplete<Dummy>);
The code is not working correctly, possibly due to it instantiates a totally different instance compared with the instance in the shared library.
In my case, I don't want to expose anything of the implementation, but the user is still able to inherit from the class and register the derived class to the library.