Yes, it's safe. If this was not safe, then std::shared_ptr would be pretty useless.
"b" which is an instance of B will be released at the end of function foo.
Yes, but its std::shared_ptr<A> is copied before that.
Let's look at what the last line of the function really does:
return b.get_a();
get_a() produces a std::shared_ptr<A>, which is copied to a temporary object. This temporary std::shared_ptr<A> object is then copied again to become the result of the call in main.
(Actual copies may even be elided due to return-value optimisation, but that doesn't change anything. If anything, it makes the safety even easier to understand.)
Only then is b destroyed, but the std::shared_ptr<A> in it has already been copied by that time.
The std::shared_ptr<A> instance within b is destroyed, too, but the memory for the A object is not freed. That's the whole point of std::shared_ptr -- it knows how often it was copied and frees dynamically allocated memory only when the last copy is destroyed.
"a" in the main function is a shared_ptr of B::m_a.
No. It's a std::shared_ptr<A>, but that's it. It has nothing to do with the original B::m_a anymore.
Is it safe to use "a" after "b" is released?
Yes, because there is no lasting relationship between a and b.
Basically, you are questioning the safety a fundamental feature of C++, namely returning values from functions. Returning a std::shared_ptr is not different with regards to safety than returning a std::string or an int. That's always what happens when you return by value from a function: The to-be-returned value is copied, the original is destroyed, the copy lives on.