Pull to refresh

Comments 38

auto myLambda = [](int x, int y = 0){ std::cout << x << '-' << y << '\n'; };

std::cout << myLambda(1, 2) << '\n';
std::cout << myLambda(1) << '\n';

Этот код выводит следующее:

1-2
1-0

конечно же нет.. будет ошибка:

no match for «operator<<» (operand types are «std::ostream» {aka «std::basic_ostream»} and «void»)

Начиная с C++14 мы можем заставить их принимать любой тип:

auto myLambda = [](auto&& x){ std::cout << x << '\n'; };

очень странно использовать здесь move симантику, если это не threads

очень странно использовать здесь move симантику, если это не threads

Не понятно в чём проблема?

Да, автор опечатался, кроме указания опечатки можно предлагать исправления:

myLambda(1, 2);
myLambda(1);

очень странно использовать здесь move симантику, если это не threads

Универсальная ссылка, может быть lvalue, может быть rvalue. Еще и проще читается, чем const auto& x. По-моему, сплошные преимущества.

Здесь не move-симантика, а perfect forwarding. И как move-семантика коррелирует с threads?

PS: Смысл данной статьи не понимаю от слова совсем, какая-то протокольная распечатка эпизода Cᐩᐩ Weekly. В чем полезность?

Интереснее всего передавать лямбду в виртуальную функцию.
    auto fn1 = []() { std::cout << 123 << std::endl; };
    using TT = decltype(fn1);

    struct A {
        virtual void a(TT fn) { fn(); }
    };

    struct B : public A {
        void a(TT fn) override { fn(); fn(); }
    };

    B a;
    a.a(fn1);
Конечно упс, Вы же в нее пихаете значение другого типа. Какой вопрос, такой и ответ. Если такое хотите, то юзайте параметр типа function<void()>.

Замените одну строку:

using TT = std::function<void()>;

Будет работать. Только говорят что std::function<T> медленный. Ручаться не буду, но имейте ввиду.

Теперь следующий вопрос: как вы предлагаете экспортировать такую функцию из динамической библиотеки?

Там нужно типа такого:


virtual void a(const std::function<void()>& fn);

При этом использование std::function действительно чуть медленнее, но не существенно. Основные тормоза буду, если будет void a(std::function<void()> fn), особенно при рекурсиях, т.к. объект std::function будет каждый раз конструироваться, и в довесок занимать лишние ~32 байта в стеке, против 8 байт, если передается по ссылке.

И как это потом использовать не из c++. Я к тому что лямбды сделаны чуть более чем через жопу не правильно.

Что мешало сделать что-то подобное?
#include <iostream>

struct Callback {
	typedef void (*fn_t)(void *ctx); fn_t fn;void* ctx;
	void operator() () { if (fn) fn(ctx); }
	Callback(fn_t fn=0,void* ctx=0) : fn(fn), ctx(ctx) {}
	template<class Q>Callback(Q q) {
		struct L { static void fn(void *ctx) { Q* q=(Q*)ctx; (*q)(); } };
		fn=L::fn; ctx=(void*)&q;
	}
};

auto fn1 = []() { std::cout << 123 << std::endl; };
using TT = Callback;

struct A {
    virtual void a(TT fn) { fn(); }
};

struct B : public A {
    void a(TT fn) override { fn(); fn(); }
};


int main(int argc, char const *argv[]) {
    B b;
    b.a(fn1);
    b.a([](){ std::cout<<sizeof(TT)<<std::endl; });

    Callback f=fn1;
    f();
    return 0;
}

Извините, а как вы собрались использовать виртуальные функции не из С++?

Это тоже в C++ требует дополнительных костылей.

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


Ваш пример не оптимален для С++, поскольку не удобно поддерживать различные параметры вызова. std::function это шаблонная структура, как раз предком которой могла бы быть структура подобная Вашей.


И Ваш пример не будет универсальным для вызовов сторонними языками, т.к. как они каждый специфичен.

А один только я не согласен с тем, что использование auto вместо конкретного типа параметра лямбды чисто для читаемости — это хорошая идея? Одно дело безобидное и уменьшающее громоздкость auto в конструкции auto var = new SuperNameSpace::GreatClass();, а другое дело в параметре лямбды, где мы таким образом теряем возможность compile-time проверки, что переданный тип соответствует ожиданиям. Я бы лучше уж алиас этому длинному типу объявил.
Так две лямбды с одинаковой сигнатурой имеют совершенно разный тип. Поэтому тут auto очередной костыль без которого будет просто треш.

Вопрос, почему так изначально заложили? Почему нужно использовать костыль типа std::function, вместо того, что бы лямбды имели сразу подобные сигнатуры. Для этого были какие-то ограничения? Тип указателя на функцию замечательно работает и имеет определяемую сигнатуру независимо от значения. Чем не устроил тип указателя на лямбды по подобной же схеме? В момент появления стандарта про лямбды, до этого не было лямбд или чего-либо подобного, поэтому ограничений по обратной совместимости быть не должно было. Зачем было так все портить?


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

Тут фокус в том, что нельзя так просто заинлайнить косвенный вызов по "указателю на лямбду". А вот прямой вызов через уникальный тип инлайнится без проблем.

Не совсем понятно, что такое косвенный и прямой вызовы относительно лямбд?


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


Инлайнить ли функции и лямбды, компиляторы уже давно определяют это самостоятельно в зависимости от контекста. Например, лямбду можно вызывать рекурсионно, используя std::function. В этом случае, она никак не инлайн и внутри std::function сохраняется как указатель.

std function это просто 4 указателя (на функцию, контекст функции, и такая же фигня для освобождения ресурсов). Проблема как всегда у C++ всё должно быть уникально, а потом это заносят в стандарт, при этом всегда оправдываются эффективностью которой можно будет достичь но потом. Указатели на виртуальный функции, на конструкторы, лямбды, коротины и даже шаблоны — всё сделаны так что бы пользователь страдал. В результате имеем ворох костылей библиотек от настрадавшихся, которые пытаются сгладить эти углы.

В том-то и дело, что в С++ лямбды не являются указателями. Они по значению передаются.


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

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


Возьмём для примера одну из реализаций std::sort в стандартной библиотеке:


https://github.com/llvm-mirror/libcxx/blob/4dde9ccef57d50e50620408a0b7a902f0aba803e/include/algorithm#L3897


Один из параметров — компаратор, _Compare __comp. Это — лямбда, переданная пользователем. Сейчас при вызове этой лямбды компилятор просто ставит прямой вызов функции _Compare::operator (), который на одном из этапов оптимизации инлайнится.


Теперь представим, что компаратор передан не через уникальный тип, а по указателю с общим типом. Теперь компилятору недостаточно информации чтобы заинлайнить его вызов до тех пор, пока он не заинлайнит вызов std::sort и всех обёрток вплоть до __sort. А в функции __sort, между просим, 200 строк. А незаинлайненый вызов лямбды — это косвенный вызов функции, который мало того что существует — так ещё и плохо "дружит" с процессором.


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

Я разве против что они по значению передаются. Я про то передставьте: есть сущность у неё есть имя, есть указатель на сущность. То в C++ вы не можете получить указатель на сущность у которой есть имя. Например битовые поля, члены класса, конструкторы и т.п. Каждый раз новые правила вводятся. Для чисел с плавющей точкой есть два штатных типа float и double они тоже по значению передаются, но типов 2. А для лямбд каждый раз тип униальный. Всё равно что тип 1.0, 1.1, тип e или тип 0. Для типа 0 тоже можно специальные оптимизации делать.
Ваш пример с sort это вообще-то шаблон функции. И потом если у вас есть compile time обработка, что мешало ввести compile type тип поток кода, с операциями над ним. Вот где поле для оптимизаций было бы.
что мешало ввести compile type тип поток кода, с операциями над ним

Так ввели же, лямбда называется...

Да вы что: вы можете изменять и анализировать этот поток?

Да, забыл совсем про ваш пример с "типом 0". Подобные типы — не такая и глупость как можно было бы подумать, и периодически применяются. Более широкому их применению мешают два факта: не так часто нужны параметры-константы, а когда нужны — зачастую можно передать их как параметр шаблона.


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


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

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

Вы, если что, кроме адреса конструктора не можете иметь ещё и адрес деструктора. Потому что единственная причина их использовать - placement new - лишает объект контекста, а компилятор - возможности следить за временем жизни, и обязывает вас явно вызывать их для используемого вами типа. Максимально явно.
Хотя у меня есть претензия к языку, почему нельзя было сделать delete (ptr) Type; вместо явного вызова деструктора?

Перечисленные вами сущности являются виртуальными. У нас есть масса других сущностей, те же перечисления enum class, которые создают свой собственный тип. Вас же не смущает, что два перечисления с одним именем и размером имеют разные типы, и если они оба окажутся в одном пространстве имён, будет ошибка компиляции?

То же для лямбд. То, что их сигнатуры вызова совпадают - не означает, что они одинаковы. Например, одна из лямбд может быть встроена в код, другая - стать функцией, а третья - целым классом. А раз они не одинаковы, то они не могут быть взаимозаменяемы. Поэтому и используется std::function - он оборачивает разные лямбды (и не только) в общий код вызова. Что, в общем, почти гарантированно заставляет компилятор создать функцию, а то и класс лямбды там, где он бы мог её заинлайнить.

Как в C++ выглядит указатель на сущность 1бит?
struct { int x:1,y:1,z:1; } value;

O'reilly? Ну, давайте по пунктам:
struct V{ int x:1; } value, values[32];
cout << "pointer " << is_pointer<V>::value; // pointer 0
&(value.x); // error: attempt to take address of bit-field
cout << sizeof(value); // 4
cout << sizeof(values); // 256

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

При этом, на современных 64-битных системах можно реализовать адресацию битов даже без оверхеда по памяти. Благо, объёмы ОЗУ даже на суперкомпьютерах и близко не скребут потолок максимального значения указателя.

Вы не поняли. В C++ указатель давно не просто число, а еще куча мистических атрибутов, для оптимизации конечно же. Иначе бы не нужны были бы пляски с бубнами. Чего стоят указатели на члены классов, или вот такие выкрутасы. А вот указателей на регистры, биты в C++ нет.
В том-то и дело, что в С++ лямбды не являются указателями. Они по значению передаются.

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


void sort(auto begin, auto end, const auto& compare)

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


Соответсвенно, вызов:


void sort(auto begin, auto end, auto compare)

Будет вести себя как при передаче по значению, для любого типа компаратора.


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


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


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


Я по прежнему не понимаю, в чем проблемность типов описанных синтаксисом, что они принципиально отличаются от типов compile time.

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

Ни в каком, а почему оно должно быть где-то написано?


Соответсвенно, вызов: [...] Будет вести себя как при передаче по значению, для любого типа компаратора.

А вот это неверно. Для уникального типа компилятор может решать инлайнить лямбду или нет. Для общего типа в случае достаточно сложного алгоритма варианта "инлайнить" попросту нет. Ну невозможно заинлайнить неизвестную функцию!


Лямбда на техническом уровне представляет структуру, с сохраненными в ней указателем на код и значениями замыкания (и это кажется было в стандарте)

А вот и нет. Нету там указателя на код, в том-то и фокус! И именно для избавления от этого указателя и нужны уникальные типы для лямбд.

а почему оно должно быть где-то написано?

Потому что иначе оно делается так как решат разработчики компилятора. Ничем не запрещается им ее не инлайнить. С чего Вы взяли, что все лямбды обязательно инлайны? Я вот на 99% уверен, что при компиляции в отладочном режиме лямбды не инлайнятся. Опять же, зависит от компилятора.


Для уникального типа компилятор может решать инлайнить лямбду или нет.

Он может это решать для любого типа, если это не ограничено стандартом или какими-либо проблемами реализации.


А вот и нет. Нету там указателя на код, в том-то и фокус!

Лямбда замечательно организовывается в любые рекурсии. Не хвостовую рекурсию никак не заинлайнишь. Хотя указателя на код здесь не нужен, он вкомпиливается в код, а не в структуру. Если бы разработчикам компилятора было нужно по другому, им это стандарт не запрещает. Но указатель на код все равно есть, и это свойство используется в std::function.


struct Node {
    Node* less = nullptr;
    Node* more = nullptr;
};

Node* root = ...;

std::function<int(Node*)> countNodes = [&](Node* node) -> int {
    int result = 1;
    if (node->less) result += countNodes(node->less);
    if (node->more) result += countNodes(node->more);
    return result;
};

std::cout << "countNodes" << countNodes(root) << std::endl;
Потому что иначе оно делается так как решат разработчики компилятора. Ничем не запрещается им ее не инлайнить. С чего Вы взяли, что все лямбды обязательно инлайны? Я вот на 99% уверен, что при компиляции в отладочном режиме лямбды не инлайнятся.

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


Он может это решать для любого типа, если это не ограничено стандартом или какими-либо проблемами реализации.

Он может что-то там решать только если ему хватает информации. То самое ограничение проблемами реализации.


Лямбда замечательно организовывается в любые рекурсии. Не хвостовую рекурсию никак не заинлайнишь. [...]

Всё написанное вами никак не мешает отсутствовать указателю на код в структуре уникального типа лямбды.

Для чего конкретно эти лямбда выражения применяются в реальных задачах?

Для передачи кода в код для колбэков короче.

как заметили выше коллбэки, и кроме того, я бы обощил до любого места, где применяется объект-замыкание \ функциональный объект

Sign up to leave a comment.