Can a type be used as another one

Templates are great! Until you call them wrong and you see the error message.

More than 200 lines error messages that exposes implementation internals even the author of a function has forgotten, or does not know where they come from, are usually not that funny.

Today I wanted to transform such an error message into something more nice. Therefore I wanted to ask if a type is a certain one, can be used or converted to an other one, and if not, use static assert to print human readable text.

Joining strings without writing a loop or use recursion

Given a function like this

join_with("/", "a", "b") // "a/b"
join_with("/", std::string{"a"}, "b", "cc") // "a/b/cc"
join_with("/", std::string{"a"}, "b", "cc", "ddd", "e") // "a/b/cc/ddd/e"

You get the idea, yes?

After some training in how to use and expand parameter packs, the implementation is not that difficult and looks like this.

template<typename... strings>
std::string join_with(const std::string& what,
                      const std::string& head,
                      const strings&... tail)
  static_assert(can_be_used_as<std::string, strings...>{},
      "only std::string compatible arguments allowed, "
      "please convert given arguments to string");
  std::string out{head};
  auto join = [&](const auto& s){out+= what+s; };
  (void)join  ; // maybe unused, in case of join_with ("/", "a") ;
  int dummy[] = { 0, ( (void) join(tail), 0) ... };
  (void)dummy;  // for sure unused
  return out ;

Today I will focus on the first line, the `static_assert can_be_used_as ` part.

This is the line that makes the different between 200 or 4 lines of error messages.

Type traits

static_assert assert needs a boolean value known at compile. Type traits are one way to get such a value.

True or False types

Type traits are types which are either a true_type or a false_type. The standard library defines 2 for us

  • std::true_type

  • std::false_type

This might sound a bit strange when reading it first, but is actually pretty easy. Those types are defined as followed:

  • typedef integral_constant<bool, true> true_type;

  • typedef integral_constant<bool, false> false_type;

and a integral_constant looks, very simplified like this

template<typename T, T V>
struct integral_constant
  static constexpr T                    value = V;
  constexpr T operator()() const { return value; }

To picture it, we can think about true_type and false_type like this

struct true_type
  constexpr bool operator()() const { return true; }

struct false_type
  constexpr bool operator()() const { return false; }

No magic here.

Create own type traits

We either derivate from true_type or false_type, or create a integral_constant<bool, VALUE> with a VALUE that we get from the compiler.

This is easy to understand seeing the code.

A simple example

We ask the compiler if 2 types are the same. Therefore we need a template which takes 2 types. For the case where the types are the same, what means there is just one type, we create a specialization.

If the compiler picks the specialization, than we know we have the same type → true_type

If the compiler picks the other version, than we know we have not the same type → false_type

A implementation might look like this.

template<typename, typename>
struct is_same
: public false_type { };

template<typename T>
struct is_same<T, T>
: public true_type { };

is_same comes with your standard library, no need to implement it your own.
But I think it is a nice example that helps to demystify type traits.

We can also combine existing type traits by using the basic boolean operations AND, OR, NOT and give the result as VALUE into a integral_constant<bool, VALUE>.
Depending on what VALUE is, we end up with either a true_type or a false_type.

A own true or false type

For the above function, join_with, I get a list of types and I want to know if a type is either

  • a string,

  • derivated from a string.

  • can be converted to a string,

In any of those 3 cases I can create a new string out of it and it is the good case (true_type). If not, than this is bad case (false_type).

So I combine the existing trait checks

  • is_same

  • is_base_of

  • is_convertible

to see if the given types to join_with can be used.

And since join_with works with and arbitrary long type list (parameter pack), it needs to combine all checks for all types into one result via parameter pack expansion.

The implementation looks like followed

// only WANTED type available
template<typename WANTED, typename...>
struct can_be_used_as : std::true_type

// Processes all given types
template<typename WANTED, typename HEAD, typename... TAIL>
struct can_be_used_as<WANTED, HEAD, TAIL...>
    : std::integral_constant<bool,
                            (std::is_same<WANTED,HEAD>{} ||
                             std::is_base_of<WANTED,HEAD>{} ||
                             std::is_convertible<HEAD,WANTED>{}) &&
                            can_be_used_as<WANTED, TAIL...>{}>

That’s it. This helps to tell at compile time if all given arguments to the function join_with can be used to concatenate strings. If not, 4 lines readable that tell you the problem are printed, not 200.

Complete code to play around with here


Feedback from reddit
is_base_of includes the is_same check, so this is redundant in the code above.
Also, with C++17, the join lambda in the join_with function can be replaced using fold expressions.
Thanks redditsoaddicting!

Thanks minirop and pkrysiak for reporting typos!

Thanks for reading

As usual, if you find any mistakes, in the code or in my spelling, please let me know.
Ideas for improvements, or any other feedback, is also very much welcome.