Пустая базовая оптимизация для лямбда-захватов — запрещена стандартом? Почему?

Недавно я столкнулся с ситуацией, когда у меня оказалось большое количество вложенных лямбда-выражений для построения цепочек асинхронных вычислений< /а>.

template <typename F>
struct node : F
{
    node(F&& f) : F{std::move(f)}
    {
    }

    template <typename FThen>
    auto then(FThen&& f_then)
    {
        return ::node{[p = std::move(*this), t = std::move(f_then)]()
        {   
        }};
    }
};

int main()
{
    auto f = node{[]{ }}.then([]{ }).then([]{ });
    return sizeof(f);
}   

Все объекты, которые я фиксирую в своих лямбда-выражениях, пусты, однако размер конечного объекта больше единицы: пример на gcc.godbolt.org.

Если я изменю лямбду внутри node</* ... */>::then на функциональный объект с явным EBO, размер конечного объекта станет равным единице.

template <typename P, typename T>
struct node_lambda : P, T
{
    node_lambda(P&& p, T&& t) : P{std::move(p)}, T{std::move(t)}
    {
    }

    void operator()()
    {
    }
};

template <typename FThen>
auto node</* ... */>::then(FThen&& f_then)
{
    return ::node{node_lambda{std::move(*this), std::move(f_then)}};
}

Live example on gcc.godbolt.org


Меня это очень раздражает, потому что я вынужден либо:

  • Напишите много шаблонного кода, который примерно эквивалентен лямбда-выражению.

  • Заплатите дополнительную стоимость памяти из-за того, что что-то вроде EBO не применяется к лямбда-захватам.

Есть ли в стандарте что-то, что явно заставляет пустые лямбда-выражения занимать дополнительное место? Если да, то почему?


person Vittorio Romeo    schedule 12.07.2017    source источник
comment
Не совсем ответ. Но я получаю EBO для этот код.   -  person StoryTeller - Unslander Monica    schedule 12.07.2017


Ответы (5)


Из expr.prim.lambda.capture :

Для каждой сущности, захваченной копией, в типе замыкания объявляется безымянный нестатический член данных.

Пока у лямбд здесь нет захвата:

auto f = node{[]{ }}.then([]{ }).then([]{ });

и, следовательно, не имеют безымянных нестатических элементов данных и, следовательно, пусты, это не то, что на самом деле использует then(). Он использует это:

return ::node{[p = std::move(*this), t = std::move(f_then)](){}};

эта лямбда захватывает t и p путем копирования и, следовательно, имеет два безымянных нестатических члена данных. Каждый .then() добавляет еще одну переменную-член, даже если каждая из них пуста, поэтому размер узла продолжает расти.

Или, другими словами, оптимизация пустой базы применяется только к базам, а захват для лямбда-выражений не создает базы, он создает нестатические элементы данных.

person Barry    schedule 12.07.2017
comment
Было бы безумием предлагать, чтобы лямбда-выражения вели себя не так, как обычные классы, и применять что-то вроде EBO? В любом случае, нет никакого способа публичного доступа к лямбда-членам. - person Vittorio Romeo; 12.07.2017
comment
@VittorioRomeo Insane полностью в глазах смотрящего. Но мне определенно кажется разумным, что если сущность захвачена копией и пуста, то она должна быть базой. - person Barry; 12.07.2017
comment
согласен, но это может привести к нарушению существующего стандартного кода (например, использование is_base_of в лямбда-выражении). - person Vittorio Romeo; 12.07.2017
comment
@Vittorio Написать статью? Я могу пересмотреть его, если хотите. Трудно представить, что это нанесет значительный вред существующему коду... - person Barry; 12.07.2017
comment
Я хотел бы сделать это, хотя сейчас работаю над другим документом. Я ценю тот факт, что вы готовы просмотреть его — я найду вас в Slack, когда у меня будет что-то приемлемое. - person Vittorio Romeo; 12.07.2017
comment
@vittorio Какой-то способ разрешить пустым переменным-членам с различным типом (без общих оснований или префиксов) совместно использовать память может быть полезен вне лямбда-выражений. Если бы был один способ сделать класс более компактным, то добавление предложения, разрешающего лямбда-выражениям иметь эту функцию, могло бы быть чище, чем добавление этой функции только для лямбда-выражений... - person Yakk - Adam Nevraumont; 12.07.2017
comment
Несколько быстрых моментов: 1. init-capture изначально были определены как объявляющие именованные элементы данных (что было бы несовместимо с EBO), прежде чем они были изменены на неименованные единицы; отчасти поэтому init-capture не может быть расширением пакета. 2. Как будто и [expr.prim.lambda.closure]/2, ничто в стандарте не мешает реализациям оптимизировать эти пустые init-capture с помощью какого-то специального механизма, но сделать это сейчас было бы перерывом ABI для существующих реализаций. , так что не задерживайте дыхание. 3. Разрешить взлом деривации было бы ужасной, ужасной идеей. - person T.C.; 12.07.2017
comment
@Barry Нет, какой-то способ иметь пустые подобъекты нулевого размера - это нормально. Обязать или разрешать получение лямбда-выражений из захвата — это неправильно. - person T.C.; 12.07.2017
comment
Будет ли (воображаемая) лямбда, полученная из захвата, наследовать операторы? (например, operator (), даже в приватном разделе)? - person Tomilov Anatoliy; 16.07.2017

У других ответов есть причина, поэтому я не буду повторяться. Я просто добавлю, что мне удалось превратить ваш пример в пример, основанный на наследовании, без лишнего шаблонного кода. Поскольку вы выполняете публичное наследование в OP, я решил удалить c'tor и перейти к агрегатной инициализации.

Потребовалось всего два руководства по дедукции, чтобы сделать код почти таким же красивым, как ваша исходная схема:

Прямая трансляция на Coliru

#include <utility>
#include <iostream>

struct empty {
    void operator()() {}
};

template <typename P, typename T>
struct node : P, T
{
    template <typename FThen>
    auto then(FThen&& f_then)
    {
        return ::node{std::move(*this), std::forward<FThen>(f_then)};
    }

    void operator()() {
        P::operator()();
        T::operator()();
    }
};

template <typename P>             node(P)    -> node<P, ::empty>;
template <typename P, typename T> node(P, T) -> node<P, T>;

int main()
{
    auto f = node{[]{ }}.then([]{ }).then([]{ });
    std::cout << sizeof(f);
}   

EBO был применен, в чем вы можете убедиться, перейдя по ссылке.

Кстати, поскольку мы движемся *this, возможно, стоит уточнить r-значение node::then. Просто чтобы избежать всякой гадости.

person StoryTeller - Unslander Monica    schedule 12.07.2017

Учитывая правило «как если» и [expr.prim.lambda. закрытие]/2:

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

  • размер и/или выравнивание типа укупорочного средства,
  • является ли тип закрытия тривиально копируемым (пункт [класс]),
  • является ли тип замыкания классом стандартной компоновки (пункт [класс]) или
  • является ли тип замыкания классом POD (пункт [класс]).

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

Тем не менее, это будет перерывом в ЛПИ, так что не задерживайте дыхание.


С другой стороны, разрешение или требование, чтобы реализация делала тип захваченной пустой переменной базой типа замыкания, была бы ужасно плохой идеей. Рассмотреть возможность:

struct X { };
struct Y { };
void meow(X x);                     // #1
void meow(Y y);                     // #2
void meow(std::function<void()> f); // #3

template<class T, class U>
void purr(T t, U u) {
    meow([t = std::move(t), u = std::move(u)] { /* ... */ });
}

Было бы безумием, если бы purr делал что-либо, кроме вызова #3, но если захваты могут стать базами, то он может вызывать #1 или #2 или быть двусмысленным.

person T.C.    schedule 12.07.2017
comment
Спасибо за цитату из Стандарта и за пример против захватов как баз. Считаете ли вы, что есть место для документа, который заставляет лямбда-выражения оптимизировать хранилище для пустых классов (например, гарантированная оптимизация пустого хранилища для замыканий), или более уместно отправлять запросы функций против gcc/clang? - person Vittorio Romeo; 12.07.2017
comment
@VittorioRomeo Учитывая поломку ABI и, как правило, незначительное преимущество, я сомневаюсь, что вы получите то, что хотите, с любым вариантом. - person T.C.; 12.07.2017

Как отмечали другие, лямбда-выражения указываются для захвата как переменных-членов, а не как баз. Так что вам не повезло.

Что вы могли сделать, так это взять страницу из привязки.

Предположим, у вас есть кортеж, который действительно использует пустую базовую оптимизацию. Затем мы можем написать помощник:

template<class Sig>
struct lambda_ebo_t;
template<class F, class...Args>
struct lambda_ebo_t<F(Args...)>:
  private std::tuple<Args...>,
  private F
{
  decltype(auto) operator()(){
    return std::apply( (F&)*this, (std::tuple<Args...>&)*this );
  }
  template<class...Ts>
  lambda_ebo_t( F f, Ts&&...ts ):
    std::tuple<Args...>( std::forward<Ts>(ts)... ),
    F( std::move(f) )
  {}
};

template<class F, class...Args>
lambda_ebo_t<F, std::decay_t<Args>...>
lambda_ebo( F f, Args&&...args ) {
  return {std::move(f), std::forward<Args>(args)...};
}

Это куча шаблонов и неполнота (захват ссылок может работать неправильно, даже если вы используете std::ref), но это дает нам:

template <typename FThen>
auto then(FThen&& f_then)
{
    return ::node{lambda_ebo([](auto&& p, auto&& t)
    {   
    }, std::move(*this), std::move(f_then))};
}

где мы храним данные вне лямбды и передаем их в качестве аргументов лямбде. Хранилище использует EBO.

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

Это один без использования кортежа, но он не поддерживает фундаментальные типы, такие как int или другие вещи, из которых вы не можете получить:

template<class Sig>
struct lambda_ebo_t;
template<class F, class...Args>
struct lambda_ebo_t<F(Args...)>:
  private Args...,
//  private std::tuple<Args...>,
  private F
{
  decltype(auto) operator()(){
    //return std::apply( (F&)*this, (std::tuple<Args...>&)*this );
    return ((F&)(*this))((Args&)*this...);
  }
  template<class...Ts>
  lambda_ebo_t( F f, Ts&&...ts ):
    Args(std::forward<Ts>(ts))...,
    F( std::move(f) )
  {}
};

template<class F, class...Args>
lambda_ebo_t<F(std::decay_t<Args>...)>
lambda_ebo( F f, Args&&...args ) {
  return {std::move(f), std::forward<Args>(args)...};
}

Живой пример с этим тестовым кодом:

auto test = lambda_ebo( [](auto&&...args){std::cout << sizeof...(args) << "\n";}, []{} , []{}, []{}, []{}, []{}, []{}, []{}, []{}); //
std::cout << "bytes:" << sizeof(test) << "\n";
std::cout << "args:";
test();

sizeof(test) это 1, и он "захватывает" 8 аргументов.

person Yakk - Adam Nevraumont    schedule 12.07.2017
comment
Это хорошее решение. Я еще не пробовал, но нельзя ли вывести из Args... без использования кортежа? Применение может быть изменено на сгиб. - person Vittorio Romeo; 12.07.2017
comment
@VittorioRomeo Вы не можете получить от int. И я предположил, что tuple уже сделал сжатие EBO. Если это не так, вы можете написать тип EBO, который поддерживает функциональность, подобную применению. Написать хороший тип кортежа EBO сложно, поэтому я не стал этого делать. Я полагаю, здесь хороший тип не нужен, так как вы не хотите хранить int. - person Yakk - Adam Nevraumont; 12.07.2017
comment
@VittorioRomeo Хорошо, сделал один без tuple и добавил живой пример. - person Yakk - Adam Nevraumont; 12.07.2017

Пустая базовая оптимизация у меня работает в следующем случае

#include <utility>

template <typename F>
class Something : public F {
public:
    Something(F&& f_in) : F{std::move(f_in)} {}
};

int main() {
    auto something = Something{[]{}};
    static_assert(sizeof(decltype(something)) == 1);
}

Живой пример здесь https://wandbox.org/permlink/J4m4epDUs19kp5CH

Я предполагаю, что причина, по которой это не работает в вашем случае, заключается в том, что лямбда, которую вы используете в методе then(), на самом деле не пуста, она имеет переменные-члены - те, которые перечислены в вашем захвате. Так что настоящей пустой базы там нет.

Если вы измените последнюю строку своего кода, чтобы она просто возвращала node{[]{}}, тогда это сработает. Лямбда-выражения, используемые .then(), не материализуются как "пустые" классы.

Принимая во внимание, что в случае явной структуры у него нет переменных-членов как таковых, только классы, от которых он наследуется, поэтому здесь может работать пустая базовая оптимизация.

person Curious    schedule 12.07.2017
comment
у него есть переменные-члены — да, p и t. - person StoryTeller - Unslander Monica; 12.07.2017
comment
Я понимаю, почему я не получаю EBO, извините за путаницу. Я спрашиваю, есть ли какая-то конкретная причина, по которой лямбда-захваты не обрабатываются аналогично EBO, поскольку нет возможности явного доступа к элементам данных лямбда, а struct-основанный решение, использующее EBO, значительно более стандартно. - person Vittorio Romeo; 12.07.2017
comment
@VittorioRomeo Я предполагаю, что, поскольку компилятору не разрешено удалять список захвата (из-за возможных побочных эффектов), он не может обрабатывать пустую лямбду как пустую базу без каких-либо переменных-членов. Я предполагаю, что это оптимизация, которая теоретически может работать, но она не реализована, и стандарт, вероятно, идет вразрез с этой оптимизацией (из-за захвата) StoryTeller answer описывает, как вы можете получить эффекты с помощью лямбда-выражений в их ответе с большим эффектом. - person Curious; 12.07.2017