Захват лямбды в другой лямбде может нарушить квалификаторы const

Рассмотрим следующий код:

int x = 3;
auto f1 = [x]() mutable
{
    return x++;
};
auto f2 = [f1]()
{
    return f1();
};

Это не скомпилируется, потому что f1() не является const, а f2 не объявлен как изменяемый. Означает ли это, что если у меня есть библиотечная функция, которая принимает произвольный аргумент функции и записывает его в лямбду, мне всегда нужно сделать эту лямбду изменчивой, потому что я не знаю, что могут передавать пользователи? Примечательно, что перенос f1 в std::function кажется, решает эту проблему (как?).

Ответ 1

Означает ли это, что если у меня есть библиотечная функция, которая принимает произвольный аргумент функции и записывает его в лямбду, мне всегда нужно сделать эту лямбду изменчивой, потому что я не знаю, что могут передавать пользователи?

Это дизайнерское решение для вашей библиотеки API. Вы можете требовать, чтобы клиентский код передавал функциональные объекты с помощью operator() const -qualified operator() (который имеет место для mutable лямбда-выражений non-). Если передается что-то другое, возникает ошибка компилятора. Но если контекст может потребовать аргумент объекта функции, который изменяет его состояние, тогда да, вы должны сделать внутреннюю лямбду mutable.

Альтернативой может быть отправка возможности вызывать operator() для экземпляра const -qualified данного типа функции. Что-то в этом духе (обратите внимание, что это требует исправления для функциональных объектов как с const и с operator() non- const operator(), что приводит к неоднозначности):

template <class Fct>
auto wrap(Fct&& f) -> decltype(f(), void())
{
   [fct = std::forward<Fct>(f)]() mutable { fct(); }();
}

template <class Fct>
auto wrap(Fct&& f) -> decltype(std::declval<const Fct&>()(), void())
{
   [fct = std::forward<Fct>(f)]() { fct(); }();
}

Примечательно, что перенос f1 в std :: function, кажется, решает эту проблему (как?).

Это ошибка в std::function из-за ее семантики стирания типов и копирования. Это позволяет вызывать operator() non- const -qualified operator(), что можно проверить с помощью следующего фрагмента:

const std::function<void()> f = [i = 0]() mutable { ++i; };

f(); // Shouldn't be possible, but unfortunately, it is

Это известная проблема, по которой стоит ознакомиться с жалобой Titus Winter.

Ответ 2

Я начну с вашего второго вопроса. Тип std::function стирает и содержит копию функтора, с которым он инициализирован. Это означает, что существует слой косвенности между std::function::operator() и фактическим operator() функтора operator().

Представьте, если хотите, удерживая что-то в вашем классе указателем. Затем вы можете вызвать операцию мутации в pointee из функции-члена const вашего класса, потому что она не влияет (в поверхностном представлении) на указатель, который содержит класс. Это похоже на то, что вы наблюдали.

Что касается вашего первого вопроса... "Всегда" - это слишком сильное слово. Это зависит от вашей цели.

  1. Если вы хотите легко поддерживать мутирующие функторы, вам следует захватить изменчивую лямбду. Но будьте осторожны, это может повлиять на библиотечные функции, которые вы можете вызывать сейчас.

  2. Если вы хотите отдавать предпочтение не мутантным операциям, то не изменяемая лямбда. Я говорю "одолжение", потому что, как мы заметили, систему типов можно "одурачить" с дополнительным уровнем косвенности. Таким образом, подход, который вы предпочитаете, будет проще в использовании, а не невозможным. Как гласит мудрый совет, сделайте правильное использование вашего API легким, а неправильное сложнее.