12

I want to use a lambda as a custom comparator in a std::map but unfortunately Visual Studio 2013 compiler does not allow using a simple code like that:

auto cmp = [](int l, int r) { return l < r; };
std::map<int, int, decltype(cmp)> myMap(cmp);
myMap[1] = 1;

and fails with

error C3497: you cannot construct an instance of a lambda

Seems that this code works fine in GCC 5.1 and Visual Studio 2015 (checked using ideone and VC++ online compiler). But for VS2013, one of the solutions would be to use a reference as proposed here (note auto&):

auto& cmp = [](int l, int r) { return l < r; };
std::map<int, int, decltype(cmp)> myMap(cmp);
myMap[1] = 1;

Clearly, GCC doesn't compile this due to binding a non-const reference to a temporary, while VS2015 issues a warning about the use of a non-standard extension. One could also use a const reference instead, but then the following code will not compile (note mutable - I'm stretching it a little by having a stateful comparator):

int compCounter = 0;
const auto& cmp = [&compCounter](int l, int r) mutable { ++compCounter; return l < r; };
std::map<int, int, decltype(cmp)> myMap(cmp);
myMap[1] = 1;

So, I see two ways of getting around this, while at the same time having the code compatible with VS2013. First,

int compCounter = 0;
auto cmp = [&compCounter](int l, int r) mutable { ++compCounter; return l < r; };
std::map<int, int, decltype(cmp)&> myMap(cmp);
myMap[1] = 1;

But this makes me think about Stephan T. Lavavej's talk on how passing raw references as explicit template parameters might be wrong if internally it is used in a template type deduction context - he talks about this at exactly this point in his presentation.

The other approach is to use a std::reference_wrapper:

int compCounter = 0;
auto cmp = [&compCounter](int l, int r) mutable { ++compCounter; return l < r; };
std::map<int, int, std::reference_wrapper<decltype(cmp)>> myMap(cmp);
myMap[1] = 1;

So my question finally is: is it guaranteed by any means that passing a raw reference type as comparator is safe? Or it depends on the STL implementers and it might break in some case and, therefore, using reference_wrapper is the way to go?

One final note: I think that passing a reference (in any form) might be useful outside of VS2013 world in case for some reason one doesn't want the comparator to be copied.

Cheers, Rostislav.

Edit: One more difference:

int compCounter = 0;
auto cmp = [&compCounter](int l, int r) mutable { ++compCounter; return l < r; };

//using cmpT = decltype(cmp)&;
using cmpT = std::reference_wrapper<decltype(cmp)>;

std::map<int, int, cmpT> myMap(cmp); 
myMap[1] = 1;

// Will work in both cases of cmpT
std::map<int, int, cmpT> m2(myMap);

// Will work only for reference_wrapper
std::map<int, int, cmpT> m2(cmp);
m2 = myMap;
Community
  • 1
  • 1
Rostislav
  • 3,857
  • 18
  • 30
  • That's weird I would think that's perfectly legal code ... You could choose to use `std::function` though. That should be almost as efficient –  Aug 26 '15 at 21:32
  • 1
    Out of curiosity, what does VS2013 make of: const std::function f = [](int l, int r) { return l < r; }; const std::function g = f; – patros Aug 26 '15 at 22:02
  • 1
    So curious. Why would the lambda be constructed in the implementation of `std::map`. I would think it's copy constructed from the argument.. –  Aug 26 '15 at 22:08
  • @AaryamanSagar Yes, I think it would be possible. But I would doubt the efficiency would be the same quite strongly. Apart from an allocation needed in case a lambda has a large state and doesn't allow small function optimization, it is always an additional indirection because of type erasure and compilers have troubles inlining them (for a nice and short explanation see [here](http://stackoverflow.com/a/16733258/3589890)), so performance might differ a lot. – Rostislav Aug 26 '15 at 22:34
  • @patros Well, I believe it's as simple as it makes a copy of the lambda object and then erases its type (again, an allocation might occur depending on the size of lambda, in case of stateless lambda like in your example - it likely will not because SFO will kick in). – Rostislav Aug 26 '15 at 22:36
  • @AaryamanSagar Yeah, it's "interesting". But given that it's only VS2013 behaviour, it might be an STL implementation bug which was fixed in VS2015. However, your comment inspired an additional potential difference between ref wrap and raw ref. See my edit. – Rostislav Aug 26 '15 at 22:39
  • 2013 should be able to compile this code. – Puppy Aug 26 '15 at 22:45
  • @Puppy If you refer to the first code snippet - it should, but it doesn't :) – Rostislav Aug 26 '15 at 23:19
  • I think when an `std::function` is storing a simple stateless function, like yours in the first example it should have similar performance. Given that it is not repeatedly copied around.. Correct me if I'm wrong though. I have never really looked into `std::function` –  Aug 27 '15 at 00:00
  • @AaryamanSagar Well, the best way is to measure it, right? :) On average, after 100 iterations of inserting 30'000 random numbers in a map of doubles to doubles, generated by mt19937 (with the same seed every time!), average speedup was about 20-25% when using lambda on my machine and cpp.sh, while on https://ideone.com/B77hIy it is up to 40-50%. – Rostislav Aug 27 '15 at 00:46
  • I guess `std::function` is indeed a heavyweight mechanism that cannot be optimized by the compiler –  Aug 27 '15 at 00:59

2 Answers2

5

The error message cannot construct an instance of a lambda is in fact an optimization bug in the STL. It is stated that this only happens with lambdas that do not capture anything, so the proposed workaround is to capture a dummy variable. (I did not actually test this; I could not find an old enough online compiler for Visual C++.)

Using a reference to to lambda instead of the lambda itself also avoids this optimization, so const auto& cmp = ... works, too. Const reference fails for mutable lambdas, because decltype(cmp) carries that const qualifier over into the map, as opposed to map(cmp) receiving a const reference and then creating a non-const copy. The code in Dietmar Kühl's answer creates a non-const reference and thus works.

Using references as template arguments

I am not really an expert here, but I will try anyway.

As Dietmar has stated in his answer, the comparator has to be CopyConstructible. The obvious explanation is that the container constructor receives it as const reference and then creates an internal copy.

When you use CompareClass & as template argument, it does not matter if CompareClass is itself CopyConstructible, because references always are. However, in this case map will hold a copy of the reference and not a copy of the object itself.

The obvious consequence is that you will have to make sure that the referenced object is not freed prematurely. Also, all copies will reference the same object instead of each having its own copy. Except that, nothing bad should happen.

So if you can keep track of references and are sure that they all will die before the object itself, I'd say it is safe. On the other side, it might not be transparent enough, and someone might misunderstand your code and break it. Also, note that after auto &a = ..., decltype(a) will be reference type too, which is even more obscure.

Note on stateful map comparator

In case of Visual Studio, map internally calls the comparator from a const-qualified method. This means that the comparator's operator() should also be const-qualified. That is, a stateful comparator will have to "pretend" to be stateless, e.g. store the state in mutable fields or in other objects stored by reference. Storing the comparator as reference type works, too.

honk
  • 9,137
  • 11
  • 75
  • 83
EvgEnZh
  • 699
  • 8
  • 13
  • Thanks for an excellent link! It does answer the question why it's working in VS2015 and not in VS2013. I'm still curious if it is completely safe to pass a raw reference as a template argument to the `std::map` according to the Standard or it could go wrong as in the talk by Stephan that I linked. Thus I'll leave the question open in case a Standard-guru decides to come by :) – Rostislav Aug 27 '15 at 01:42
  • *"I did not actually test this, could not find old enough online compiler for Visual C++"* Works fine! http://rextester.com/TVHSI85700 – dyp Aug 27 '15 at 17:14
  • I can confirm: Putting a dummy variable into the capture specification works for VS2012. Even using `[this]` works. – honk Feb 18 '18 at 21:17
1

First, note that the lambda expression is a temporary, not a const object. It is possible to bind it to an rvalue reference just fine:

int compCounter = 0;
auto&& tmp = [compCounter](int l, int r) mutable { ++compCounter; return l < r; };
auto&  cmp = tmp;
std::map<int, int, decltype(cmp)> myMap(cmp);
myMap[1] = 1;

This code first binds the lambda object to an rvalue reference. Since an rvalue reference is an lvalue, the name can be bound to an lvalue reference. The lvalue reference can then be used with the std::map<...>.

Aside from being able to compare key types, the only requirement I can find on the comparison object is that it is CopyConstructible (in Table 102 "Associative container requirements"). Base on std::is_copy_constructible<decltype(cmp)>::value it is CopyConstructible.

That code certainly compiles with gcc and clang. I don't have MSVC++ accessible to check if it also compiles with MSVC++.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Good point, Dietmar. However, I am pretty sure that this code is equivalent to the code snippet that creates a lambda object and then uses `decltype(cmp)&` - it creates an object on stack (either temporary which has its lifetime prolonged by binding an rvalue reference or just a named object) and then uses the `compiler_invented_lambda_name&` as the template parameter. The question still stands - could something bad happen when a raw reference type is a template parameter - as it is in the video I linked, or any Standard-conforming STL implementation is guaranteed to work correctly? – Rostislav Aug 26 '15 at 23:28
  • And yes, it compiles with VS2013, but so does the snippet after "First," (no idea how to better refer to it...) – Rostislav Aug 26 '15 at 23:31