Here’s a puzzler for you: given the code below, what does the for
loop print?
using HandlersMap = std::map<std::string, std::function<std::string(const std::string&)>>; using MetaMap = std::map<std::string, HandlersMap>; void insert(MetaMap& meta, const std::string& key, const std::string& sub) { auto handlers = meta[key]; handlers[sub] = [=](const std::string& msg) { return "The response for " + sub + " is: " + msg; }; } int main(int argc, char **argv) { MetaMap meta; insert(meta, "get", "/api/v1/test1"); insert(meta, "get", "/api/v1/test2"); insert(meta, "post", "/api/v1/test3"); for (auto m : meta) { cout << m.first << ":" << endl; for (auto h : m.second) cout << "\t" << h.first < " << h.second("message") << endl; } }
You would be forgiven for assuming that this would be the output:
get: /api/v1/test1 --> The response for /api/v1/test1 is: message /api/v1/test2 --> The response for /api/v1/test2 is: message post: /api/v1/test3 --> The response for /api/v1/test3 is: message
what you get instead is a rather disappointing:
get: post:
the difference being a tiny weeny &
: in order to get the desired output, you would have to change this line:
// Note the change to auto& auto& handlers = meta[key];
Or, even better, use this:
meta[key][sub] = [=](const std::string& msg) { return "The response for " + sub + " is: " + msg; };
This is even the more confusing, by looking at the signature of operator[]
in the stl::map
header:
// In stl_map.h mapped_type& operator[](const key_type& __k)
which states rather unequivocally that you will be getting a reference to whatever value was stored, in relation to the key __k
(or, a reference to a newly-created, and thence stored, instance of mapped_type
: this being the root of the requirement, for value types in STL maps, to have a default constructor).
This was driving me crazy while developing a “mapped routes” handling mechanism for an API Server I am designing (more on this in another blog post) until I figured out that the “nested” invocation (meta[key][sub]
) was working, while the “split” call was not.
As specified in the Rules for auto
resolution, when used for a variable initializer, the compiler will “[use] the rules for template argument deduction from a function call”; in this case, the absence of any modifier, leads the compiler to detect the type of handler
as U
as if an imaginary function f
were defined as:
template<typename U> void f(U expr); // Now invoke f() to determine the type to replace `auto` with. f(meta[key]);
thus implicitly converting the reference type of the returned value, into an actual class type (std::map<K, V>
).
This has the rather undesired side-effect of turning the assignment into a copy constructor of the map stored as the value, handlers
: a local variable, which will be destructed upon exiting from the function, and any changes thereof to be irretrievably lost.
However, using auto&
, we convert the above into:
template<typename U> void f(U& expr);
thus yielding the expected reference type and, ultimately, the desired outcome.
Leave a Reply