GCC 7.X C++17 template auto parameter type deduction with enum class bug

Viewed 90

I noticed a probable bug in GCC 7.X but I can't find anything recognizing this bug anywhere. It would be nice to know if this was ever reported as a compiler bug or was hidden and until you know about it, you will assume that it works as it should. Especially that the feature is listed as supported for this version.

Until now I used this check to determine if I can use template auto patameters.

#if (__cpp_template_auto >= 201606L) || (__cpp_nontype_template_parameter_auto >= 201606L)
    #define TEMPLATE_AUTO_AVAILABLE 1
#endif

In older code you'd have to declare template like that:

template <typename EnumType, EnumType enumValue>
auto func() { exclusive version for EnumType::enumValue }

Then call it like that:

auto value = func<MyEnumType, MyEnumType::value>();

With auto template parameter it is tempting to use it like this:

template <auto enumValue>
auto func() { exclusive version for decltype(enumValue)::enumValue }
auto value = func<MyEnumType::value>();

The problem is that it is not an equivalent in GCC 7.X and can lead to nasty bugs.

Minimal example code:

enum class FirstEnum : short { firstVal = 7 };
enum class SecondEnum : size_t { secondVal = 7 };

template <typename T>
struct TypeName;

template <>
struct TypeName<FirstEnum>
{
    static constexpr auto name = "FirstEnum";
};

template <>
struct TypeName<SecondEnum>
{
    static constexpr auto name = "SecondEnum";
};


template <auto t>
void func()
{
    std::cout << TypeName<decltype(t)>::name << std::endl;
}

int main()
{
    func<FirstEnum::firstVal>();
    func<SecondEnum::secondVal>();
}
2 Answers

It seems as if template was instantiated for first integer value instead of actual template parameter. Because of that it does not distinguish enum classes by their type and instantiates single template for first enum class that is used with the template. All following uses with different enum types will be linked with first instantiated version. It does not respect enum class underlying type as well.

Godbolt example: https://godbolt.org/z/8P9vEn

In slightly more complicated example you can notice that the template is actually not generated per enum class type and value but some other type. This thing I'm not able to comprehend. You can see that decltype(t) is different in template parameter list and in template function itself and oddly enough, it does not depend on the type of "t" but function parameter type. Well, this answer leads to more questions than it really answers. https://godbolt.org/z/sorbMr

GCC 7.X is not newest version of GCC but is still commonly used, especially in embedded solutions. Avoiding such known bugs is quite crucial in many applications and obviously it is better to be aware of those issues. Feel free to add more information if you understand how it was broken or how to work around this issue and still be able to use auto parameter to deduce enum class type.

In case godbolt link did not work in future, here is extended example:

#include <type_traits>
#include <typeinfo>
#include <iostream>
#include <memory>
#include <string>
#include <cxxabi.h>
#include <limits>

inline std::string demangle(const char* name)
{
    int status = -1;
    std::unique_ptr<char, void(*)(void*)> res {
        abi::__cxa_demangle(name, nullptr, nullptr, &status),
        std::free
    };
    return (status == 0) ? res.get() : name;
}

template<typename T>
std::string fullTypeName()
{
    return demangle(typeid(T).name());
}

template <typename EnumType>
constexpr auto extractEnum(EnumType val)
{
    return static_cast<std::underlying_type_t<std::remove_reference_t<EnumType>>>(val);
}

enum class FirstEnum : unsigned char { val = 0 };
enum class SecondEnum : size_t { val = 0 };
enum class ThirdEnum : int { val = 0 };

template <auto t, typename T = decltype(t)>
void func(T param)
{
    std::cout << fullTypeName<decltype(t)>()
        << "/" << fullTypeName<T>()
        << " size T: " << sizeof(decltype(t))
        << " size t: " << sizeof(T)
        << " value: " << param << std::endl;
}

int main()
{
    func<FirstEnum::val>(0);
    func<FirstEnum::val>(0U);
    func<SecondEnum::val>(500);
    func<SecondEnum::val>(50000000000000000);
    func<ThirdEnum::val>(-1);
}

Result on GCC 7.X:

FirstEnum/int size T: 1 size t: 4 value: 0
FirstEnum/unsigned int size T: 1 size t: 4 value: 0
FirstEnum/int size T: 1 size t: 4 value: 500
SecondEnum/long size T: 8 size t: 8 value: 50000000000000000
FirstEnum/int size T: 1 size t: 4 value: -1

Result on GCC 8.1+ or Clang:

FirstEnum/int size T: 1 size t: 4 value: 0
FirstEnum/unsigned int size T: 1 size t: 4 value: 0
SecondEnum/int size T: 8 size t: 4 value: 500
SecondEnum/long size T: 8 size t: 8 value: 50000000000000000
ThirdEnum/int size T: 4 size t: 4 value: -1

You can use this workaround as long, as you don't declare any parameter of type T

template <auto t, typename T = decltype(t)>
void func()
{
    std::cout << TypeName<decltype(t)>::name << std::endl;
}

This way T is the actual type of t and forces the template to be instantiated separately. https://godbolt.org/z/Y967Ps

This solution will not work if you declare parameter of type T as it will be deduced from function parameter type instead of the default template type parameter.

I'm posting this as a separate answer in case you are looking for workaround, not explanation. This way you can vote for the answer that is more useable for others in your opinion.

Related