Использование ссылки на лямбда в качестве компаратора на карте (правильный путь?)

Я хочу использовать лямбда как пользовательский компаратор в std::map, но, к сожалению, компилятор Visual Studio 2013 не позволяет использовать простой код:

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

и с ошибкой

ошибка C3497: вы не можете создать экземпляр lambda

Кажется, что этот код отлично работает в GCC 5.1 и Visual Studio 2015 (проверено с помощью ideone и онлайн-компилятор VС++). Но для VS2013 одним из решений было бы использовать ссылку, предложенную здесь (примечание auto &):

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

Очевидно, что GCC не компилирует это из-за привязки неконстантной ссылки на временную, в то время как VS2015 выдает предупреждение об использовании нестандартного расширения. Вместо этого можно использовать ссылку на константу, но тогда следующий код не будет компилироваться (обратите внимание на изменчивый - я немного растягиваю его с помощью компаратора с состоянием):

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;

Итак, я вижу два способа обойти это, в то же время имея код, совместимый с VS2013. Во-первых,

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;

Но это заставляет меня задуматься о том, как Стефан Т. Лававей говорит о том, как передавать исходные ссылки, поскольку явные параметры шаблона могут быть неправильными, если внутренне они используются в контексте вывода типа шаблона - он говорит об этом именно в этот момент в его презентации.

Другой подход заключается в использовании 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;

Итак, наконец, мой вопрос: гарантируется ли это каким-либо образом, что передача ссылочного типа в качестве компаратора безопасна? Или это зависит от исполнителей STL, и в некоторых случаях это может сломаться, и поэтому использование reference_wrapper - это способ пойти?

Последнее замечание: я думаю, что передача ссылки (в любой форме) может оказаться полезной вне мира VS2013, если по какой-то причине не требуется копировать компаратор.

Cheers, Ростислав.

Изменить: Еще одно отличие:

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;

Ответ 1

Сообщение об ошибке cannot construct an instance of a lambda на самом деле является ошибкой оптимизации в STL. Утверждается, что это происходит только с лямбдами, которые ничего не захватывают, поэтому предлагаемое обходное решение заключается в захвате фиктивной переменной. (Я на самом деле не тестировал это, я не мог найти достаточно старый онлайн-компилятор для Visual С++.)

Использование ссылки на лямбда вместо самой лямбда также позволяет избежать этой оптимизации, поэтому const auto& cmp = ... тоже работает. Ошибка константы для изменяемых lambdas, потому что decltype(cmp) переносит этот спецификатор const на карту, в отличие от map(cmp), получая ссылку на const, а затем создает неконстантную копию. Код в Dietmar Kühl отвечает, создает неконстантную ссылку и, следовательно, работает.

Использование ссылок в качестве аргументов шаблона

Я не эксперт здесь, но я все равно попробую.

Как сказал Дитмар в своем ответе, компаратор должен быть CopyConstructible. Очевидное объяснение заключается в том, что конструктор контейнера принимает его в качестве ссылки на константу, а затем создает внутреннюю копию.

Когда вы используете CompareClass & как аргумент шаблона, не имеет значения, является ли CompareClass самим CopyConstructible, потому что ссылки всегда есть. Однако в этом случае карта будет содержать копию ссылки, а не копию самого объекта.

Очевидным следствием является то, что вы должны быть уверены, что указанный объект не будет освобожден досрочно. Кроме того, все копии будут ссылаться на один и тот же объект, а не на каждую свою копию. Кроме того, ничего плохого не должно произойти.

Итак, если вы можете отслеживать ссылки и уверены, что все они умрут перед самим объектом, я бы сказал, что это безопасно. С другой стороны, это может быть недостаточно прозрачным, и кто-то может неправильно понять ваш код и сломать его. Также обратите внимание, что после auto &a = ..., decltype(a) тоже будет ссылочным типом, что еще более неясно.

Примечание по компаратору карты состояния

В случае Visual Studio, map внутренне вызывает компаратора из метода const-qual. Это означает, что компаратор operator() также должен быть const-квалифицированным. То есть, компонент сравнения состояний должен будет "притворяться" безгражданством, например. сохранять состояние в изменяемых полях или в других объектах, сохраненных по ссылке. Сохранение компаратора в качестве ссылочного типа тоже работает.

Ответ 2

Во-первых, обратите внимание, что выражение лямбда является временным, а не const объектом. Его можно привязать к значению rvalue просто:

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;

Этот код сначала привязывает лямбда-объект к ссылке rvalue. Поскольку ссылка rvalue является значением lvalue, имя может быть привязано к ссылке lvalue. Затем ссылку lvalue можно использовать с std::map<...>.

Помимо возможности сравнивать типы ключей, единственное требование, которое я могу найти на объекте сравнения, заключается в том, что оно CopyConstructible (в таблице 102 "Требования к ассоциативным контейнерам" ). Основание на std::is_copy_constructible<decltype(cmp)>::value равно CopyConstructible.

Этот код обязательно компилируется с помощью gcc и clang. У меня нет MSVС++, доступного для проверки, также ли он компилируется с MSVС++.