This is the first episode of a new serie, "A C++ developer's logbook".
In this article, we will see how unused code mays lead to unexpected template instantiation.
Disclaimer : Obviously, I'm terrible at naming things. Any suggestion is welcome.
Consider the following scenario :
We want to create a helper function, that helps us to construct template-template-types values.
Disclaimer :
For simplicity sake, I will only use std::tuple as generated values container in the following code-snippets.
Of course in this very case, std::tuple<Ts...>::tuple<Args...>(Args&&...) is a better choice.
We provide two set of variadic templates :
values_types
: types used to resolve our template-template parameterHere, a
std::tuple<values_types...>
args_types
: types of argument values used to constructvalues_types
So here are the rules :
We can expect that bothvalues_types
andargs_types
contain the same number of types :
>static_assert(sizeof...(values_types) == sizeof...(args_types));
Andargs_types
may be use as parameter tovalues_types
constructors
>static_assert((std::is_constructible_v<values_types, args_types&&> && ...));
Here is the use-case :
We create a generator of std::tuple
, then use it to create a std::tuple<int, std::string>
value from an int
and a char const (&)[5]
generator<std::tuple> my_tuple_generator;
auto my_tuple_value = my_tuple_generator.create<int, std::string>(42, "toto");
So our initial class definition could be :
template
<
template <typename ...> class container_type
>
struct generator
{
template
<
typename ... values_types,
typename ... args_types
>
constexpr decltype(auto) create(args_types&& ... args);
};
Using our class definition form the previous section, we can implement our class just like :
template
<
typename ... values_types,
typename ... args_types
>
constexpr decltype(auto) create(args_types&& ... args)
{
static_assert(sizeof...(values_types) == sizeof...(args_types));
static_assert((std::is_constructible_v<values_types, args_types&&> && ...));
return container_type<values_types...>{ std::forward<args_types>(args)... };
}
And it works just find !
(see the complete implementation on godbolt)
Basically, we just called std::tuple<Ts...>::tuple<Args...>(Args&&...).
Sometimes, it is just more convinient to pass your parameter as a std::tuple
instead of a variadic-template.
But how to perform the parameter pack expansion then ?
What I wanted to do here was to add another function signature, that allows args_types&&... args
to be pass as a args_container_type<args_types&&...> && arg
.
This way, args_container_type
could be any type that support std::get
and std::tuple_size
, just like std::tuple, std::array and std::pair do.
template
<
typename ... value_types,
typename ... args_types
>
constexpr decltype(auto) create(args_types&& ... args);
template
<
typename ... value_types,
typename ... args_types,
template <typename...> class args_container_type
>
constexpr decltype(auto) create(args_container_type<args_types...> && arg);
The obvious solution to expand the parameter-pack from a tuple in this case is to use std::apply.
We do not want to add extra code with std::integer_sequence and std::get
.
template
<
typename ... value_types,
typename ... args_types,
template <typename...> class args_container_type
>
constexpr decltype(auto) create(args_container_type<args_types...> && arg)
{
return std::apply
(
generator::create<value_types...>, /* does not compile */
std::forward<args_container_type<args_types...>>(arg)
);
}
Of course, this does not compile.
We need to create a callable object from our member-function generator::create_impl<value_types...>
.
Here I wondered, what is the fastest in this case ?
Two options :
- Use generic a lambda
[this](args_types && ... args)
{
return create<values_types...>(std::forward<args_types>(args)...);
}
- Use std::bind on a member function
std::bind(&generator::create<value_types...>, this)
So I quickly benchmarked this two options. Click here to see the benchmark on quick-bench.com.
The result was that using std::bind
4.3 times faster that using a generic-lambda
.
Let's bring all the pieces together.
template
<
typename ... value_types,
typename ... args_types,
template <typename...> class args_container_type
>
constexpr decltype(auto) create(args_container_type<args_types...> && arg)
{
return std::apply
(
std::bind(&generator::create<value_types...>, this),
std::forward<args_container_type<args_types...>>(arg)
);
}
Now, let's try a quick code that will generate our template functions :
auto main() -> int
{
generator<std::tuple> generator_value;
// Instantiate `constexpr decltype(auto) create(args_types&& ... args)`
// use case 0 : (std::is_same_v<value_types, args_types> && ...)
auto my_tuple_0 = generator_value.create<int, char>(42, 'a'); // ok, variadics args, same types
// use case 1 : !(std::is_same_v<value_types, args_types> && ...)
auto my_tuple_1 = generator_value.create<int, std::string>(42, "toto"); // ok, variadics args, different types
}
It worked fine.
And of course, it generated the expected code :
using expected_result_type = std::tuple<values_types...>;
using result_type = decltype(generator_value.create<values_types...>(args_types...));
static_assert(std::is_same_v<result_type, expected_result_type>);
However, what happend if we instantiate our second function, constexpr decltype(auto) create(args_container_type<args_types...> && arg)
?
auto my_tuple_2 = generator_value.create<int, char>(std::forward_as_tuple(42, 'a')); // std::tuple, same types
Then we have the following compiler output (using MSVC2019, v16.0.4):
(Test it on godbolt.org using clang (trunk))
error C3528: 'args_types': the number of elements in this pack expansion does not match the number of elements in 'values_types'
note: see reference to function template instantiation
'decltype(auto) generator<std::tuple>::create<int,char,>(void)' being compiled
note: see reference to function template instantiation
'decltype(auto) generator<std::tuple>::create<int,char,int&&,char&&,std::tuple>(std::tuple<int &&,char &&> &&)' being compiled
error C2607: static assertion failed
It seems that the call to decltype(auto) generator<std::tuple>::create<values_types...>(args_types...)
is not the one we expected here.
decltype(auto) generator<std::tuple>::create<int,char,>(void)
/\ /\
here ---/ /
and here ---/
Well, obviously, args_types...
expanded to nothing but void
.
The probleme here is the way we reference a template member function.
In order to reference a (template) member function, we need to provide a complete signature.
And the signature we provide was :
generator<container_type>generator::create<value_types...>, this)
,
so no args_types...
here, because this second parameter-pack is resolved using the function parameters.
This excluse trying to hack using generator::create<value_types..., args_types...>
, as both parameter-packs will expand into one.
note: see reference to function template instantiation :
'decltype(auto) generator<std::tuple>::create<int,char,int&&,char&&,>(void)' being compiled
A convinient work-around for our problem is to create a wrapper that will explicitly resolve the two parameter-packs distinctly.
Here, the wrapper is called creator
and is a private inner struct.
template <typename ... values_types>
struct creator
{
template <typename ... args_types>
static constexpr decltype(auto) create(args_types&& ... args)
{
static_assert(sizeof...(values_types) == sizeof...(args_types));
static_assert((std::is_constructible_v<values_types, args_types&&> && ...));
return container_type<values_types...>{ std::forward<args_types>(args)... };
}
};
This way, we now have :
template
<
typename ... values_types,
typename ... args_types
>
constexpr decltype(auto) create(args_types&& ... args)
{
return creator<values_types...>::template create<args_types...>(std::forward<args_types>(args)...);
}
template
<
typename ... values_types,
typename ... args_types,
template <typename...> class args_container_type
>
constexpr decltype(auto) create(args_container_type<args_types...> && arg)
{
using creator_type = generator<std::tuple>::creator<values_types...>;
return std::apply
(
creator_type::template create<args_types...>,
std::forward<args_container_type<args_types...>>(arg)
);
}
And it works just fine.
See the complete sample on godbolt.org.
However, for some reasons this kind of wrapper may not be so convinient.
Mainly, if you need to share some extra logic between generator<container_type>
and creator<values_types...>
.
In this case, using a generic lambda is a good option :
template
<
typename ... values_types,
typename ... args_types
>
constexpr decltype(auto) create(args_types&& ... args)
{
static_assert(sizeof...(values_types) == sizeof...(args_types));
static_assert((std::is_constructible_v<values_types, args_types&&> && ...));
return container_type<values_types...>{ std::forward<args_types>(args)... };
}
template
<
typename ... values_types,
typename ... args_types,
template <typename...> class args_container_type
>
constexpr decltype(auto) create(args_container_type<args_types...> && arg)
{
return std::apply
(
[this](args_types && ... args)
{
return create<values_types...>(std::forward<args_types>(args)...);
},
std::forward<args_container_type<args_types...>>(arg)
);
}
When template instantiation goes wrong, this often leads to compiler error messages that are about 1k characters long. Each line.
What may come handy is to juggle with :
- On MSVC : /permissive- to enable two-phase-name-lookup
- Godbolt.org
- Good old
#pragma message (__FUNCSIG__)
Also, to my experience, when the compiler itself crash, most of the time updating it solves the problem. How funny this is.