The secret is the one definition rule. If the inlines are hidden, then you may end up with multiple (private) copies of inlines per image.
Ideally, you'd configure and use everything so that it is able to function using the one definition rule, then you might enable private externs as an optimization (which may not be good in every regard, particularly binary size). You would favor this approach because it follows the standard's model.
For a quick refresher on ODR:
// somewhere.hpp
namespace MON {
inline int cas(const int*,const int*,int*) {
return dah_dum();
}}
// elsewhere.hpp
namespace MON {
inline int cas(const int*,const int*,int*) {
return dum_dah();
}}
Any int MON::cas(const int*,const int*,int*)
reference which is not wholly or partially inlined, but a function call to int MON::cas(const int*,const int*,int*)
may result in use of either definition regardless of which definition was visible to the TU. Exactly one definition is preserved by the linker, and all definitions are assumed to all be equal. That's important because your binary sizes would explode if every definition which is referenced and visible would yield a copy for each translation.
If it 'works' when using rules of ODR, then it's likely that you have multiple definitions of a given symbol in your object files, and that you are end up referencing different definitions due to the compiler setting. If you have declared an inline which is not static or in an anonymous namespace, the definition should be the same across all source files.
If you're mixing this with C TUs, well... it has different rules for linkage which only complicates the matter further.