c++templatesc++17type-deduction

Type Deduction in Templates


I have here c++17 code which perfectly compiles with GCC and Clang:

#include <type_traits>
#include <string>
#include <map>

struct MapRegistration
{
    template <template <typename, typename, typename...> class MapT, typename KeyType, typename ValueType, typename... Rest, typename Map = MapT<KeyType, ValueType, Rest...>>
    static void from(ValueType& (MapT<KeyType, ValueType, Rest...>::*insertMethod)(KeyType const&))
    {
    }
};

int main()
{
    MapRegistration::from(&std::map<std::string, double>::operator[]);
    return 0;
}

The goal here is to save a member function pointer of different map implementations (I.e. std::map, QMap etc.).

Under Visual Studio there seems to be an overloaded method in contrast to the GCC and Clang implementation.

And I get following error:

<source>(15): error C2672: 'MapRegistration::from': no matching overloaded function found
<source>(8): note: could be 'void MapRegistration::from(ValueType &(__cdecl MapT<KeyType,ValueType,Rest...>::* )(const KeyType &))'
<source>(15): note: 'void MapRegistration::from(ValueType &(__cdecl MapT<KeyType,ValueType,Rest...>::* )(const KeyType &))': could not deduce template argument for 'MapT'
<source>(15): note: 'void MapRegistration::from(ValueType &(__cdecl MapT<KeyType,ValueType,Rest...>::* )(const KeyType &))': could not deduce template argument for 'KeyType'
<source>(15): note: 'void MapRegistration::from(ValueType &(__cdecl MapT<KeyType,ValueType,Rest...>::* )(const KeyType &))': could not deduce template argument for 'ValueType'
Compiler returned: 2

See Godbolt link.

I think the solution is to tell the compiler which version of the overloaded method the compiler should pick. But how?

Are there really good template experts here?

Thanks in advance! 🙏

EDIT:

To be more clear: My requirement is highly generic. What I want to do is to develop a Meta Type System. That is, my struct should be able to accept any kind of map and the insertion method should work with any key-value-combination.

Here a more elaborated snippet of my production code:

struct MapRegistration
{
    template <typename Map, typename KeyType, typename ValueType>
    MapRegistration(std::string mapName, ValueType& (Map::*insertMethod)(KeyType const&))
    : mapName(mapName)
    {
        using InsertMethod = ValueType& (Map::*)(KeyType const&);

        insertFunction = [mapName, insertMethod](std::any& mapAny, std::any const& keyAny, std::any&& valueAny) {
            Map* collection = std::any_cast<Map>(&mapAny);

            // Error Handling

            ValueType* value = std::any_cast<ValueType>(&valueAny);

            // Error Handling

            KeyType const* key = std::any_cast<KeyType>(&keyAny);

            // Error Handling

            constexpr bool hasCorrectInsertMethod = std::is_invocable_r<ValueType&, InsertMethod, Map, KeyType const&>();
            static_assert(hasCorrectInsertMethod, "Insert Method is not correct!");

            std::invoke(insertMethod, *collection, *key) = *value;
        };
    }

    std::string mapName;
    std::function<void(std::any&, std::any const&, std::any&&)> insertFunction;
};

With the saved insertFunction function in my Struct, I can pass any map and any key-value combination!


Solution

  • Generally it is not advisable to take addresses of library functions. The reason for this is just what you observed. A library may add an overload when this does not break things. Taking the address of a function will break. Functions in the standard library where you are guaranteed that you can take their address are explicitly specified as "addressable functions". For other functions in the standard library it is undefind.

    You don't have to use member function pointers and the "workaround" is actually the simpler and more flexible alternative: Wrap the call in another callable. For example in a lambda:

    #include <type_traits>
    #include <string>
    #include <map>
    
    struct MapRegistration
    {
        template <typename F>
        static void from(F f)
        {
        }
    };
    
    int main()
    {
        MapRegistration::from([](std::map<std::string,double>& map, const auto& value){ map.insert(value);});
    }
    

    Often it is simpler to start from the most generic and then add restricitions as appropriate rather than starting from the most specific and then trying to generalize it. If you want to restrict the type of the callable to the right signature, you can use concepts (or SFINAE).


    If the method is yours and you know you can take the address, you can pick the right overload via a static cast. For example:

    struct foo {
        void a(){}
        void a(int){}
    };
    
    int main() {
        auto f = static_cast<void (foo::*)(int)>(&foo::a);
    }
    

    This picks the second overload and type deduction works as expected.

    Now you might think that the issue explained above is solved and you can actually take any methods address. However, consider that if a library tells you: "foo has a method called a that can be called with a single int" then the cast may still fail. The real signature might be void a(int,int=0). Taking this just some steps further, there is not necessarily a member function a to enable a user to call foo{}.a(42); (it could be a member with operator()). All you need to know is that you can call it. And that you shall not use its address (unless the library explicitly says thats ok).