user1508519
user1508519

Reputation:

Variable templates + generic lambdas for std::map

An answer to C++14 Variable Templates: what is the purpose? Any usage example? proposes a usage example of variable templates + generic lambdas that would look something like this:

void some_func() {
    template<typename T>
    std::map<int, T> storage;

    auto store = []<typename T>(int key, const T& value) { storage<T>.insert(key, value) };

    store(0, 2);
    store(1, "Hello"s);
    store(2, 0.7);

    // All three values are stored in a different map, according to their type. 
}

Unfortunately it doesn't compile so I've tried to "fix" it and this is my attempt so far.

#include <map>

template<typename T>
std::map<int, T> storage;

void some_func() {

    auto store = [](int key, const auto& value) { storage<decltype(value)>.insert(key, value); };

    store(0, 2);
    store(1, std::string("Hello"));
    store(2, 0.7);
}

The error message are:

main.cpp:7:76: error: no matching member function for call to 'insert'

    auto store = [](int key, const auto& value) { storage<decltype(value)>.insert(key, value); };
                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
main.cpp:10:10: note: in instantiation of function template specialization 'some_func()::<anonymous class>::operator()<std::basic_string<char> >' requested here

    store(1, std::string("Hello"));

When you instantiate a variable template, like all templates, each variable will be a different type. The theory is auto doesn't get deduced for each type, but only one type initially (double). Therefore the code is invalid. Even if this could work, each instantiation of storage would refer to a different variable.

How can this code be rewritten to achieve the original intent?


Edit I made a small mistake in my edit (see revision history to avoid wall of text.) decltype(pair) should be decltype(pair.second) since there is only one template argument for storage.

#include <map>

template <typename T>
std::map<int, T> storage;

void some_func() {
    auto store = [&](auto pair) { storage<decltype(pair.second)>.insert(pair); };

    store(std::pair<int, int>(0, 1));
    store(std::pair<int, std::string>(1, "Hello!"));
    store(std::pair<int, int>(2, 3));
}

int main()
{
}

There are now linker errors.

/tmp/main-5f1f7c.o: In function `some_func()':
main.cpp:(.text+0x1a): undefined reference to `storage<int>'
main.cpp:(.text+0x43): undefined reference to `storage<std::string>'
main.cpp:(.text+0x74): undefined reference to `storage<int>'

In order to fix the linker errors, I think you need to explicitly instantiate the arguments? (I'm not even sure if that's the correct term here.)

template <typename T>
std::map<int, T> storage;

template <>
std::map<int, int> storage<int>;

template <>
std::map<int, std::string> storage<std::string>;

Live Example

Upvotes: 6

Views: 1788

Answers (1)

Luc Danton
Luc Danton

Reputation: 35449

template<typename T>
std::map<int, T> storage;

This is a declaration, much like e.g. template<typename T> class foo;. As we need one anyway, we would benefit from making it a definition as well:

template<typename T>
std::map<int, T> storage {};

This doesn’t get rid of the linker errors however, suggesting there’s an outstanding bug with implicit instantiation. To convince ourselves, we can trigger instantiation in various ways:

  • explicitly, which would look like

    // namespace scope, same as storage
    template /* sic */ std::map<int, int>         storage<int>;
    template           std::map<int, std::string> storage<std::string>;
    
  • implicitly, by adding the following inside main

    storage<int>.size();
    storage<std::string>.size();
    

in either case taming the linker.

What you attempted is explicit specialization, which does indeed get around the issue although using a different mechanism.

Upvotes: 4

Related Questions