Working around a possible MSVC bug when deducing multiple variadic args

Viewed 64

Here is some code that compiles in GCC (on godbolt at least - can't test locally), for handling a compile-time dependency system - the conversion operator here is to make it easier to take an entity that specifies what it can read/write and reduce it down implicitly to a more restricted form when passing into functions (please pretend that the operator below does some static_assert kind of enforcement).

template<typename... Args>
struct WriteList{};

template<typename... Args>
struct ReadList{};

template<typename Reads = ReadList<>, typename Writes = WriteList<>>
class TypedEntity;

template <typename... ReadTypes, template <typename...> typename Reads, typename... WriteTypes, template <typename...> typename Writes>
class TypedEntity<Reads<ReadTypes...>, Writes<WriteTypes...>>
{
public:
    template <typename... OtherReadTypes, typename... OtherWriteTypes>
    operator TypedEntity<ReadList<OtherReadTypes...>, WriteList<OtherWriteTypes...>>()
    {
        return {};
    }
};

struct ComponentA{};
struct ComponentB{};
struct ComponentC{};

void TestFunc1(TypedEntity<ReadList<ComponentA, ComponentB>, WriteList<ComponentC>> entity)
{

}

void TestFunc2(TypedEntity<ReadList<ComponentA>, WriteList<>> entity)
{

}

void TestFunc3(TypedEntity<ReadList<ComponentA>, WriteList<ComponentC>> entity)
{

}

int main()
{
    TypedEntity<ReadList<ComponentA, ComponentB>, WriteList<ComponentB>> entity;
    TestFunc1(entity);
    TestFunc2(entity);
    TestFunc3(entity);    

    return 0;
}

But under MSVC (latest, i.e.g 19.28, as well as some other 19.x versions I've sampled (19.14, 19.24, etc)):

error C3547: template parameter 'OtherWriteTypes' cannot be used because it follows a template parameter pack and cannot be deduced from the function parameters of 'TypedEntity<Reads<ReadTypes...>,Writes<WriteTypes...>>::operator TypedEntity<ReadList<OtherReadTypes...>,WriteList<OtherWriteTypes...>>'
  1. Is this valid C++ and MSVC is wrong?
  2. Is there a workaround for this issue in MSVC?

Appreciate it.

1 Answers

The error message hints at MSVC's mistake (emphasis mine):

error C3547: template parameter 'OtherWriteTypes' cannot be used because it follows a template parameter pack and cannot be deduced from the function parameters of 'TypedEntity<Reads<ReadTypes...>,Writes<WriteTypes...>>::operator TypedEntity<ReadList<OtherReadTypes...>,WriteList<OtherWriteTypes...>>'

It's true it can't be deduced from the function parameters - but for a conversion function, deduction happens from the return type, not the empty parameter list.

So a workaround is to simplify the template signature of the conversion function. Presuming the implementation needs to know the actual ReadTypes... and WriteTypes..., the operator() definition can just call a private ordinary member function which can deduce them from parameters.

template<typename T>
struct is_WriteList_s : public std::false_type {};
template<typename... Args>
struct is_WriteList_s<WriteList<Args...>> : public std::true_type {};
template<typename T>
concept is_WriteList = is_WriteList_s<T>::value;

template<typename T>
struct is_ReadList_s : public std::false_type {};
template<typename... Args>
struct is_ReadList_s<ReadList<Args...>> : public std::true_type {};
template<typename T>
concept is_ReadList = is_ReadList_s<T>::value;

template <typename... ReadTypes, template <typename...> typename Reads,
          typename... WriteTypes, template <typename...> typename Writes>
class TypedEntity<Reads<ReadTypes...>, Writes<WriteTypes...>>
{
private:
    template <typename... OtherReadTypes, typename... OtherWriteTypes>
    TypedEntity<ReadList<OtherReadTypes...>, WriteList<OtherWriteTypes...>>
    convert_impl(std::type_identity<TypedEntity<
        ReadList<OtherReadTypes...>, WriteList<OtherWriteTypes...>>>) const;
public:
    template <is_ReadList OtherReadList, is_WriteList OtherWriteList>
    operator TypedEntity<OtherReadList, OtherWriteList>() const
    {
        return convert_impl(
            std::type_identity<TypedEntity<OtherReadList, OtherWriteList>>{});
    }
};

I used std::type_identity just as a type-wrapper which doesn't actually have any data members or logic. If not compiling with C++20 support, any dummy template struct would do, or a raw pointer with null argument.

If using a version of MSVC or a /std: switch which does not support concepts, the concepts can be converted to SFINAE tricks or simple static_asserts.

Related