Как std :: unique_ptr не имеет размера при использовании лямбды - PullRequest
1 голос
/ 02 ноября 2019

Ответьте на этот вопрос,
Как у std :: unique_ptr не должно быть размера служебной информации?

Я выполнил следующий код, и результат оказался неожиданным:

#include <memory>

#include <cstdio>

void f(void *p){
    free(p);
}

int main(){
    auto x1 = [](void *p){ free(p); };
    auto x2 = [](void *p){ f(p); };

    printf("%zu\n", sizeof(std::unique_ptr<int>                 )); //  8, expected
    printf("%zu\n", sizeof(std::unique_ptr<int, decltype(&f)>   )); // 16, expected
    printf("%zu\n", sizeof(std::unique_ptr<int, decltype(x1)>   )); //  8, unexpected
    printf("%zu\n", sizeof(std::unique_ptr<int, decltype(x2)>   )); //  8, unexpected
}

Последние два типа с лямбдами имеют размер 8, даже если они делают то же самое, что и f().

Как это сделано?

Ответы [ 2 ]

1 голос
/ 02 ноября 2019

Лямбда без захвата не должна иметь никаких подобъектов;это просто тип с перегрузкой operator(). Таким образом, это может быть (но не обязательно) пустой тип. unqiue_ptr разрешено (но не обязательно) оптимизировать способ, которым он "содержит" тип средства удаления, так что, если тип средства удаления является пустым типом класса, он может использовать различные методы, чтобы гарантировать, что этот тип не занимаетхранилище внутри самого экземпляра unique_ptr.

Есть несколько способов сделать это. unique_ptr может наследоваться от типа, полагаясь на EBO для оптимизации базового класса. С C ++ 20 он может просто сделать его подобъектом члена, полагаясь на атрибут [[no_unique_address]] для обеспечения оптимизации пустого члена. В любом случае единственное фактическое хранилище, в котором нуждается unique_ptr<T>, это указатель на T.

В отличие от этого, указатель на функцию - это указатель на функцию. Это фундаментальный тип, который должен иметь хранилище, потому что он может указывать на любую функцию с этой сигнатурой. Тип по существу содержит функцию-член для вызова как часть самого типа;указатель на функцию не делает. Экземпляр типа на самом деле не нуждается в хранилище, чтобы найти его operator().

1 голос
/ 02 ноября 2019

Обезврежив лямбды и упрощая decltype s, вы на самом деле написали:

int main(){
    struct x1_impl {
        void operator()(void *p) { free(p); }
    };
    x1_impl x1;
    struct x2_impl {
        void operator()(void *p) { f(p); }
    };
    x2_impl x2;

    printf("%zu\n", sizeof(std::unique_ptr<int>));
    printf("%zu\n", sizeof(std::unique_ptr<int, void (*)(void*)>));
    printf("%zu\n", sizeof(std::unique_ptr<int, x1_impl>));
    printf("%zu\n", sizeof(std::unique_ptr<int, x2_impl>));
}

unique_ptr<T, Del> необходимо хранить как T*, так и Del. Так как x1_impl и x2_impl не имеют элементов данных, им не нужно хранение, поэтому последние два unique_ptr могут просто хранить T*. Обратите внимание, что decltype(&f) - это void (*)(void*), но decltype(x1) - это неназванное пустое struct. Лямбды и указатели функций на самом деле не похожи друг на друга. С указателем на функцию код для выполнения известен только во время выполнения и находится за указателем. При использовании лямбды код для выполнения известен во время компиляции, а лямбда-объект фактически является замыканием, коллекцией захваченных переменных. Здесь нет таких переменных, поэтому лямбдам не нужно ничего хранить.

...