Согласно источникам, которые я нашел, лямбда-выражение по существу реализуется компилятором, создающим класс с перегруженным оператором вызова функции и ссылочными переменными в качестве членов. Это говорит о том, что размер лямбда-выражений варьируется, и при наличии достаточного количества ссылочных переменных размер может быть сколь угодно большим .
Объект std::function
должен иметь фиксированный размер , но он должен иметь возможность обертывать любые вызываемые объекты, включая любые лямбды того же типа. Как это реализовано? Если std::function
внутренне использует указатель на свою цель, что происходит, когда std::function
экземпляр копируется или перемещается? Есть ли какие-либо выделения в куче?
std::function
время назад я изучал реализацию gcc / stdlib . По сути, это класс дескриптора для полиморфного объекта. Производный класс внутреннего базового класса создается для хранения параметров, выделенных в куче - тогда указатель на него сохраняется как подобъектstd::function
. Я считаю, что он использует подсчет ссылок, какstd::shared_ptr
при копировании и перемещении.Ответы:
Реализация
std::function
может отличаться от одной реализации к другой, но основная идея состоит в том, что она использует стирание типа. Хотя есть несколько способов сделать это, вы можете представить себе тривиальное (не оптимальное) решение, подобное этому (упрощенное для конкретного случаяstd::function<int (double)>
для простоты):В этом простом подходе
function
объект будет хранить толькоunique_ptr
базовый тип. Для каждого другого функтора, используемого сfunction
, создается новый тип, производный от базового, и динамически создается экземпляр объекта этого типа.std::function
Объект всегда одного и того же размера и выделить пространство по мере необходимости для различных функторов в куче.В реальной жизни существуют различные оптимизации, которые обеспечивают повышение производительности, но усложняют ответ. Тип может использовать оптимизацию небольших объектов, динамическую отправку можно заменить указателем свободной функции, который принимает функтор в качестве аргумента, чтобы избежать одного уровня косвенности ... но идея в основном та же.
Что касается вопроса о том, как
std::function
ведут себя копии объекта , быстрый тест показывает, что копии внутреннего вызываемого объекта созданы, а не разделяют состояние.Тест указывает, что
f2
получает копию вызываемого объекта, а не ссылку. Если бы вызываемая сущность совместно использовалась разнымиstd::function<>
объектами, на выходе программы было бы 5, 6, 7.источник
std::function
правильной, если бы внутренний объект был скопирован, и я не думаю, что это так (представьте лямбду, которая фиксирует значение и является изменяемой, хранящейся внутри astd::function
, если состояние функции было скопировано, количество копийstd::function
внутри стандартного алгоритма может привести к другим результатам, что нежелательно.std::function
инициирует выделение.Ответ от @David Rodríguez - dribeas хорош для демонстрации стирания типа, но недостаточно хорош, поскольку стирание типа также включает в себя то, как типы копируются (в этом ответе объект функции не будет копируемым). Эти поведения также сохраняются в
function
объекте, помимо данных функтора.Уловка, используемая в реализации STL из Ubuntu 14.04 gcc 4.8, заключается в том, чтобы написать одну общую функцию, специализовать ее для каждого возможного типа функтора и привести их к универсальному типу указателя функции. Поэтому информация о типе стирается .
Я придумал упрощенную версию этого. Надеюсь, это поможет
В версии STL также есть некоторые оптимизации.
construct_f
иdestroy_f
смешиваются в один указатель на функцию (с дополнительным параметром , который говорит , что делать), чтобы сохранить несколько байтunion
, так что когдаfunction
объект создается из указателя функции, он будет храниться непосредственно вunion
пространстве, а не в кучеВозможно, реализация STL - не лучшее решение, поскольку я слышал о более быстрой реализации . Однако я считаю, что основной механизм такой же.
источник
Для определенных типов аргументов («если целью f является вызываемый объект, переданный через
reference_wrapper
или указатель на функцию»),std::function
конструктор запрещает любые исключения, поэтому использование динамической памяти не может быть и речи. В этом случае все данные должны храниться непосредственно внутриstd::function
объекта.В общем случае (включая случай лямбда) использование динамической памяти (либо через стандартный распределитель, либо через распределитель, переданный
std::function
конструктору) разрешено, если реализация считает нужным. Стандарт рекомендует реализации не использовать динамическую память, если этого можно избежать, но, как вы правильно сказали, если объект функции (неstd::function
объект, а объект, заключенный в него) достаточно велик, нет способа предотвратить это, посколькуstd::function
имеет фиксированный размер.Это разрешение на создание исключений предоставляется как обычному конструктору, так и конструктору копирования, который также явно позволяет выделять динамическую память во время копирования. Для ходов нет причин, по которым может понадобиться динамическая память. Стандарт явно не запрещает это и, вероятно, не может, если перемещение может вызывать конструктор перемещения типа обернутого объекта, но вы должны иметь возможность предположить, что если и реализация, и ваши объекты разумны, перемещение не вызовет любые выделения.
источник
An
std::function
перегружает,operator()
превращая его в объект-функтор, лямбда работает точно так же. Он в основном создает структуру с переменными-членами, к которым можно получить доступ внутриoperator()
функции. Итак, основная концепция, которую следует иметь в виду, заключается в том, что лямбда - это объект (называемый функтором или объектом функции), а не функцией. Стандарт запрещает использовать динамическую память, если этого можно избежать.источник
std::function
? Это ключевой вопрос.std::function
объекты имеют одинаковый размер и не соответствуют размеру содержащихся лямбда-выражений.std::vector<T...>
объект имеет фиксированный размер (время копирования) независимо от фактического экземпляра распределителя / количества элементов.std::function<void ()> f;
нет необходимости размещать там,std::function<void ()> f = [&]() { /* captures tons of variables */ };
скорее всего, выделяет.std::function<void()> f = &free_function;
наверное, тоже не выделяет ...