Beware of C++11 `auto`

fantasy-2995326_1280

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.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: