Thursday, February 20, 2014

Efficient string arguments passing to C-functions

I often have to deal with the C-language libraries and the first thing I do in such case is writing a C++ wrapper to abstract my logic from C-style memory management, error handling, etc. One of the commonplaces here is work with strings. Wrapper of C-function with string parameters should support both std::strings and the string literals (compare this with constructors of standard exception classes which are overloaded to accept const char* or const std::string& error message).

Consider the following function:
int libfoo_do_smth(libfoo_context_t* context, const char* x); //returns TRUE on success

Wrapper should be something like
void libfoo::context::do_smth(const char* x) {
 if(!libfoo_do_smth(m_context, x))
  throw libfoo::exception("do_smth failed");
};

template<class CharTraits, class Allocator>
void libfoo::context::do_smth(const std::basic_string<char, CharTraits, Allocator>& x) {
 do_smth(x.data());
};
It's OK (except that the second overload is trivial). But what if libfoo_do_smth is taking two string arguments? Or maybe three ones ore more? Even in case of two arguments we need four overloads and we'll confront with a combinatorial explosion of number of trivial overloads increasing number of string arguments. To solve this problem I use a simple class that is a zero-ended string wrapper. It mimics the std::string interface but neither owns the memory used to store array of char nor knows the length of the string it refers to.
template<typename Char>
struct basic_const_c_string {
 template<typename C>
 basic_const_c_string(const basic_const_c_string<C> string) :
  m_data{string.data()}
 {};

 basic_const_c_string(const Char* string) :
  m_data{string}
 {
  if(!string) throw std::invalid_argument("null-pointer initialization");
 };

 template<typename CharTraits, typename Allocator>
 basic_const_c_string(
  const std::basic_string<
   typename std::remove_const<Char>::type, CharTraits, Allocator
  >& string
 ) :
  m_data{string.data()}
 {};

 template<typename C, typename Traits, typename Allocator>
 basic_const_c_string(std::basic_string<C, Traits, Allocator>&&) = delete;

 const Char* data() const {
  return m_data;
 };

 const Char* c_str() const {
  return data();
 };

private:
 const Char* m_data;
}; //basic_const_c_string

typedef basic_const_c_string<char>    const_c_string;
typedef basic_const_c_string<wchar_t> const_c_wstring;
Now there's no need in overloads and our wrapper may look like following:
void libfoo::context::do_smth(const_c_string x) {
 if(!libfoo_do_smth(m_context, x.data()))
  throw libfoo::exception("do_smth failed");
};
Obviously there's no need in overloads in case of any number of string parameters. Now however we cannot pass a temporary std::string parameter to our function. Corresponding constructor of basic_const_c_string is deleted because we cannot move std::string to basic_const_c_string (it doesn't owns memory). To circumvent this I use combination of universal references and overloading:
template<typename Char, typename String>
basic_const_c_string<Char>
make_const_string(String&& string) {
 return {string};
};

template<typename Char, typename CharTraits, typename Allocator>
const std::basic_string<Char, CharTraits, Allocator>
make_const_string(std::basic_string<Char, CharTraits, Allocator>&& string) {
 return {string};
};
So the wrapper code becomes (in case of two string parameters):
template<class String1, class String2>
void libfoo::context::do_smth_else(String1&& x, String2&& y) {
 auto temp_x = make_const_string<char>(std::forward<String1>(x));
 auto temp_y = make_const_string<char>(std::forward<String2>(y));

 if(!libfoo_do_smth_else(m_context, temp_x.data(), temp_y.data()))
  throw libfoo::exception("do_smth_else failed");
};
Well, now we need one template parameter for one string argument, but it's look better than four trivial overloads of one function.

No comments:

Post a Comment