Верно ли, что если глобальная функция использует нелокальные переменные, то это закрытие? - PullRequest
2 голосов
/ 05 мая 2020

Я был очень запутан тем, что такое замыкание в C ++. Я читал это Что такое «Закрытие»? , но почти все ответы относятся к JavaScript, но я думаю, что есть некоторые различия в закрытии между C ++ и JavaScript. Поэтому мне было трудно сопоставить описание JavaScript закрытия с C ++.

Например, почти все ответы используют функцию, возвращающую функцию, в качестве примера для демонстрации закрытия в JavaScript. Но я не нахожу подобного шаблона в C ++.

Более того, в JavaScript нет такой вещи, как «список захвата».


  1. Мне сказали, что если функция использует нелокальные переменные (из внешней или глобальной области), то это закрытие. Это правильно?

Пример 1:

    int a = 3;

    int am_I_a_closure(int c){
        return c + a;
    }

    int main(){
    }
Зачем нужен список захвата? Не может ли лямбда в C ++ работать так же, как JavaScript вложенные функции? Или, другими словами, не может ли лямбда в C ++ работать так же, как глобальная функция, обращающаяся к глобальным (нелокальным) переменным?

Я имею в виду, через обычный процесс поиска имени , если имя не найдено в текущей области видимости, то найти его во внешней области видимости, затем во внешней области видимости ...

Зачем нужен список захвата? Зачем нужно фиксировать переменные внешней области видимости? Разве это нельзя сделать с помощью обычного поиска имени?

Пример 2:

int main(){
    int a = 3;
    {
        int b = 5;
        {
            int c = 4;
            {
                std::cout << a+b+c <<std::endl;
            }
        }
    }
}

Пример 3:

int main(){
    std::vector<int> values = {1,5,3,4,3};
    int a = 3;
    std::find_if(values.begin(), values.end(), [](int value) {return value > a; }); //Error, `a` is not captured.
}

И снова, в примере 3, почему a нужно захватить вместо обычного поиска имени, как в Примере 1 и Примере 2?

1 Ответ

5 голосов
/ 05 мая 2020

Важно понимать, что «закрытие» - это концепция, которая имеет очень специфическое c значение в функциональном программировании. Однако C ++ не является функциональным языком; его не особо заботит строгое соблюдение терминологии функционального программирования. Он просто определяет различные функции, некоторые из которых могут или не могут хорошо соответствовать этой терминологии.

JavaScript и C ++ - разные языки. В JavaScript функция имеет свойство, называемое "первоклассным объектом". Это означает, что, когда вы выполняете код для создания «функции», вы создаете объект, который представляет эту функцию. Переменная, содержащая функцию, принципиально не отличается от переменной, содержащей строку, или переменной, содержащей массив, или чего-то еще. . Вы можете перезаписать переменную, содержащую функцию, массивом или наоборот.

В частности, функции как объекты первого класса могут иметь состояние, связанное с ними в момент их создания. Если такая функция выходит за пределы своей области видимости для доступа к локальной переменной, тогда эта область может быть сохранена как часть состояния функции; это состояние будет доступно автоматически, когда вы попытаетесь использовать эту переменную в функции. Получается, что вы выходите «за пределы» функции, но это не так; область видимости была перенесена с вами, и вы просто получаете к ней доступ.

В C ++ функция не является объектом первого класса. Вы можете получить указатель на функцию, но указатели функций явно отличаются от указателей объекта (преобразование между ними даже не требуется, чтобы быть действительным). В языке C ++ функция не «создается» и не «уничтожается»; каждая функция присутствует всегда, от начала программы до ее конца.

Функции C ++ могут обращаться к глобальным переменным, но это потому, что они глобальные . Местоположение глобальной переменной запекается в исполняемом файле во время компиляции / компоновки, поэтому для доступа к функции не требуется сохранять особое состояние.

Однако в C ++ есть полезная концепция, которая может помогают создать эффект первоклассного функционального объекта. А именно, тип класса может перегрузить оператор вызова функции operator(). Это позволяет вызывать экземпляр класса, как если бы он был функцией. Экземпляры классов являются объектами и могут иметь внутреннее состояние (также известное как переменные-члены), а перегрузка operator() - это просто функция-член этого типа.

Учитывая все это, вы можете создать что-то, что имитирует должным образом объект функции с ограниченной областью видимости. Все, что вам нужно, это класс, в котором есть переменные-члены, соответствующие переменным вне области действия функции, на которую он ссылается. Эти члены могут быть инициализированы в конструкторе класса, передавая конструктору внешние значения. Тогда у вас есть действительный объект, который вы можете вызвать, и он может получить доступ к этим «внешним» переменным, используя свои переменные-члены.

Это все лямбда C ++. Все это завернуто в "красивый, аккуратный" синтаксис. Он пишет для вас класс; он записывает для вас переменные-члены, которые вы «захватываете» из внешнего мира, и он вызывает конструктор и передает эти переменные за вас.

Однако , C ++ - это язык, который изо всех сил старается не чтобы сделать что-то дороже, чем вам нужно. Чем больше внешних переменных вы используете, тем больше внутренних переменных-членов потребуется лямбда-выражению, и, следовательно, чем больше будет класс и тем больше времени потребуется для инициализации / copy / et c. Поэтому, если вы хотите использовать некоторую внешнюю переменную (которая реализована как переменная-член), C ++ требует, чтобы вы либо явно перечислили ее (чтобы вы знали, что вы хотите ее захватить), либо использовать механизмы захвата по умолчанию [=] или [&] (так что вы явно отказываетесь от своего права жаловаться на случайное увеличение и / или замедление вашего лямбда-типа).

Более того, в JavaScript все является ссылкой. В переменных хранятся ссылки на массивы, функции, словари и т. Д. c. JavaScript - это справочный язык.

C ++ - это ориентированный на значения язык . Переменная в JavaScript ссылается на объект; переменная в C ++ - это объект . В C ++ нельзя заменить один объект другим; вы можете скопировать значение объекта, но это все еще тот объект.

Таким образом, становится важным то, как лямбда должна захватывать конкретную переменную. Вы можете захватывать переменные по значению (копируя значение в скрытый элемент) или по ссылке (ссылаясь на объект).

Это особенно важно, потому что C ++ не выполняет сборку мусора. Это означает, что наличие ссылки на объект не означает, что объект все еще существует . Если у вас есть переменная в стеке, и вы получаете ссылку на нее, и эта ссылка существует за точкой, где переменная стека выходит за пределы области видимости ... эта ссылка теперь бесполезна. В JavaScript было бы нормально из-за сборки мусора. Но C ++ этого не делает. У вас есть ссылка на уничтоженный объект, который нельзя использовать.

Итак, если вы хотите, чтобы лямбда захватывала локальные переменные и , вы хотите, чтобы лямбда сохранялась после точки, где переменные не больше существуют, вам нужно будет захватить такие переменные по значению , а не по ссылке.

Захват по значению или по ссылке определяется как вы перечисляете переменную в список захватов. &x означает захват по ссылке, а x - по значению. Захват по умолчанию [=] означает захват по значению по умолчанию, а [&] означает захват ссылки по умолчанию.

...