Использование ссылки на лямбда в качестве компаратора на карте (правильный путь?)
Я хочу использовать лямбда как пользовательский компаратор в 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С++.