The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal Public Domain Dedication

Mapping types in C++

In the C++ performance guideline I’ve presented the concat function as an alternative to std::string::operator+ for concatenating multiple strings together more efficiently.

I wrote down the most compact version, which has two disadvantages

  • it supports only char-based strings (no wchar_t based strings)

  • the whole implementation is a template function, thus implementation details are "leaked" in the header file

Add support for wchar_t based strings

The most naive approach would be to add a second concat function

template <class... S>
std::wstring concat(const S&... strs) {
  std::string_view views[]{strs...};
  int totalsize = std::accumulate(std::begin(views), std::end(views), 0, [](int count, std::string_view v){return count + v.size();});
  std::string result(totalsize, '\0');
  int cur_pos = 0;
  for(const auto& v : views){
    std::copy(v.begin(), v.end(), result.begin() + cur_pos);
    cur_pos += v.size();
  }
  assert(cur_pos == totalsize);
  return result;
}

It will not work, because both function signatures are the same, and the compiler is not able to decide which one to pick.

The easiest way to fix the issue is to give both functions a different name, like concat and wconcat.

Reduce the amount of templated code

I would also like to reduce the amount of code in the header file. Especially for variadic functions, a different number of parameters creates a new function. This creates code bloat and leads to long compile times.

Also since allocating is a slow operation, having the whole implementation inline does not make a measurable difference.

An easy way to reduce the code bloat is to move the implementation to another function that takes a std::span of strings:

namespace detail{
    std::string concat(std::span<const std::string_view>);
    std::wstring concat(std::span<const std::wstring_view>);
}

template <class... S>
std::string concat( const S&... strs ) {
    const std::string_view views[]{{ strs... }};
    return detail::concat( views ); // non templated function
}

template <class... S>
std::wstring wconcat( const S&... strs ) {
    const std::wstring_view views[]{ strs... };
    return detail::concat( views ); // non templated function
}


// in the .cpp file

std::string concat(std::span<const std::string_view> views) {
  int totalsize = std::accumulate(std::begin(views), std::end(views), 0, [](int count, std::string_view v){return count + v.size();});
  std::string result(totalsize, '\0');
  int cur_pos = 0;
  for(const auto& v : views){
    std::copy(v.begin(), v.end(), result.begin() + cur_pos);
    cur_pos += v.size();
  }
  assert(cur_pos == totalsize);
  return result;
}

Since the implementation is not inline, we can also drop <numeric> and <cassert> from the header file (and need to add <span>).

In this case, even if the templated concat function is instantiated multiple times, the amount of code it generates is much smaller. detail::concat has all the business logic, and is compiled only once.

overloads instead of different function names

Having concat and wconcat is not a major issue, but can be problematic in generic code.

Fortunately, there are multiple ways to have overloads in this case.

Non-templated parameter

Just add a non-templated parameter at the beginning

namespace detail{
    std::string concat(std::span<const std::string_view>);
    std::wstring concat(std::span<const std::wstring_view>);
}

template <class... S>
std::string concat( std::string_view sv, const S&... strs ) {
    const std::string_view views[]{{ sv, strs... }};
    return detail::concat( views ); // non templated function
}

template <class... S>
std::wstring concat( std::wstring_view sv, const S&... strs ) {
    const std::wstring_view views[]{ sv, strs... };
    return detail::concat( views ); // non templated function
}

Contrary to the previous implementation, it is not possible to call concat with only a parameter. If this use case needs to be supported, it is possible to add other overloads, and even avoid an unnecessary copy

std::string concat( std::string str ) {
    return str;
}
std::wstring concat( std::wstring str ) {
    return str;
}

Templated parameter

There is another approach, with more templates.

For the given example, there are no advantages, but it permits to

  • generalize the concat function a little more

  • avoid writing two identical templated functions (no matter how small)

For having a unified templated function, we need to be able to detect if we want to store the result in a char or wchar_t string.

std::string and std::string_view have a value_type, but what about char* and other string types from third-party libraries?

With std::conditional, it is possible to write char_type_map:

template <class U>
struct char_type_map {
    using type = typename std::conditional<
      std::is_constructible<typename std::string_view, U>::value,
      char,
      typename std::conditional<
        std::is_constructible<typename std::wstring_view, U>::value,
        wchar_t,
        void>::type
      >::type;
};


static_assert(std::is_same<typename char_type_map<std::string>::type, char>::value);
static_assert(std::is_same<typename char_type_map<std::wstring>::type, wchar_t>::value);
static_assert(std::is_same<typename char_type_map<int>::type, void>::value);

If a type is convertible to std::string_view, then we have a char-based string, if it is convertible to std::wstring_view, we have a wchar_t based string. If no conversion is possible, char_type_map<T>::type is void.

This can be used for providing the appropriate overloads of concat:

namespace detail{
    std::string concat(std::span<const std::string_view>);
    std::wstring concat(std::span<const std::wstring_view>);
}

template <class S1, class... S>
auto concat( const S1& str, const S&... strs ) {
  using char_type = typename char_type_map<S1>::type;
  using string_view = std::basic_string_view<char_type>;
  return detail::concat( {{ string_view(str), string_view(strs)... }} );
}

Why another template?

Considering code bloat and compile times, why use another template?

Because I also wanted to support types that are not directly convertible to string_view, but are still convertible; for example characters: concat('a', "string").

With the current implementation, this code does not compile. Either one uses "a" instead of 'a', or one converts 'a' manually to a std::string_view.

Since concat is already converting all other types to std::string_view, it makes sense to be able to use characters directly.

A custom string class from third-party libraries might have a conversion function and not a conversion operator, but it would still be nice to be able to use them directly.

Adding support for such types means special casing char_type_map for those types, and adding an internal conversion function.

The final code looks like

#include <utility>
#include <string_view>
#include <string>
#include <span>

template <class U>
struct char_type_map {
    using type = typename std::conditional<
      std::is_constructible<typename std::string_view, U>::value,
      char,
      typename std::conditional<
        std::is_constructible<typename std::wstring_view, U>::value,
        wchar_t,
        void>::type
      >::type;
};

template <>
struct char_type_map<wchar_t> {
    using type = wchar_t;
};
template <>
  struct char_type_map<char> {
    using type = char;
};



namespace detail {
  std::string concat(std::span<const std::string_view>);
  std::wstring concat(std::span<const std::wstring_view>);

  template <class C, class T>
  constexpr std::basic_string_view<C> to_view( const T& t ) {
    return std::basic_string_view<C>( t );
  }
  template <>
  constexpr std::string_view to_view( const char& c ) {  // by cref, otherwise dangling
    return std::string_view( &c, 1 );
  }
  template <>
  constexpr std::wstring_view to_view( const wchar_t& c ) {  // by cref, otherwise dangling
    return std::wstring_view( &c, 1 );
  }
}

template <class S1, class... S>
auto concat( const S1& str, const S&... strs ) -> std::basic_string<typename char_type_map<S1>::type>{
    using char_type = typename char_type_map<S1>::type;
    return detail::concat( {{ detail::to_view<char_type>( str ), detail::to_view<char_type>( strs )... }} );
}

It is a nice example for showing how to map types based on a property, but for this function, it might only be relevant when dealing with frameworks that provide their own string class (the MFC library 🗄️ has CString, the Xerces library 🗄️ has XMLString, the Qt framework 🗄️ has QString, the abseil library 🗄️ has absl::string_view, the EASTL 🗄️ library has eastl::string, and so on and so on). In the case of an application, using a non-template parameter and duplicating the two-line templated function is probably sufficient.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.