Write your API using standard layout classes.
Standard layout classes can be quite fancy.
Here is an uncompiled array_view
off the top of my head:
template<class Container>
using data_ptr_type = decltype( std::declval<Container>().data() );
template<bool b>
using bool_kt = std::integral_constant<bool, b>;
template<class T>
struct array_view {
T* b = nullptr;
T* e = nullptr;
T* begin() const { return b; }
T* end() const { return b; }
template<class U>
using ptr_is_compatible = bool_kt<
std::is_same< U, T* >{} || std::is_same< U, std::remove_const_t<T>* >{} ||
std::is_same< U, std::remove_volatile_t<T>* >{} || std::is_same< U, std::remove_cv_t<T>* >{}
>;
// convert from .data() and .size() containers:
template<class In,
std::enable_if_t<
ptr_is_compatible< data_ptr_type< In& > >{}, int
> = 0
>
array_view( In&& in ):array_view(in.data(), in.size()) {}
// special ones:
array_view()=default;
array_view(array_view const&)=default;
array_view& operator=(array_view const&)=default;
// manual ones:
array_view( T* ptr, std::size_t N ):array_view(ptr, ptr+N) {}
array_view( T* s, T* f ):b(s), e(f) {}
// from C-style array:
template<class U, std::size_t N,
std::enable_if_t<
ptr_is_compatible< U* >{}
,int> = 0
>
array_view( U(&arr)[N] ):array_view(arr, N) {}
template<class Container>
Container copy_to() const {
return {begin(), end()};
}
};
While fancy, it is standard layout. So only the most insane ABI changes will break it.
Now your header file reads:
extern __attribute__((visibility("default"))) void foo(array_view<const char>, int);
and the caller can call it with foo( "hello", 7 )
or foo( std::string("hello"), 42 )
or foo( std::vector<char>{'a', 'b', 'c'}, 18 )
or whatever. You don't care.
Pointers to the begin-end of the buffer are done on the callers side, so you don't know the layout of the thing passed.
On the inside, you can marshal back to a container with a arg.to<std::string>()
if you need it.