
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 comment