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
- Update
-
Feedback from reddit
is_base_of
includes theis_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.