Skip to content

Instantly share code, notes, and snippets.

@GuillaumeDua
Last active April 9, 2024 20:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GuillaumeDua/af7d31232058f8b470ea153fc784700a to your computer and use it in GitHub Desktop.
Save GuillaumeDua/af7d31232058f8b470ea153fc784700a to your computer and use it in GitHub Desktop.
C++ : When template instantiation goes wrong

C++ : When template instantiation goes wrong

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.

Table of content

Introduction

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 parameter

    Here, a std::tuple<values_types...>

  • args_types : types of argument values used to construct values_types

    So here are the rules :

    We can expect that both values_types and args_types contain the same number of types :
    > static_assert(sizeof...(values_types) == sizeof...(args_types));

    And args_types may be use as parameter to values_types constructors
    > static_assert((std::is_constructible_v<values_types, args_types&&> && ...));

Basic design

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);
};

Initial implementation

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&&...).

Extended implementation

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...>.

std::bind vs generic-lambdas

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)...);
    }
    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.

Implementation : first attempt

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>);

When things go wrong

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

What's wrong ?

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 ---/

Why ?

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

Implementation : second attempt

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.

Implementation : Third attempt

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)
    );
}

Conclusion

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 :

Also, to my experience, when the compiler itself crash, most of the time updating it solves the problem. How funny this is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment