Beware of C++11 `auto` | Code Trips & Tips on WordPress.com
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 handler = meta[key];
handler[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.
Originally published at codetrips.com on December 10, 2017.