Какое соглашение о вызове по умолчанию для функции лямбда С++? - программирование
Подтвердить что ты не робот

Какое соглашение о вызове по умолчанию для функции лямбда С++?

Следующий код был скомпилирован с VС++ 2012:

void f1(void (__stdcall *)())
{}

void f2(void (__cdecl *)())
{}

void __cdecl h1()
{}

void __stdcall h2()
{}

int main()
{
    f1(h1); // error C2664
    f2(h2); // error C2664

    f1([](){}); // OK
    f2([](){}); // OK

    auto fn = [](){};

    f1(fn); // OK
    f2(fn); // OK
}

Я думаю, что ошибки нормальны, но ОК являются ненормальными.

Итак, мои вопросы:

  • Что такое вызывающее соглашение для лямбда-функции С++?

  • Как указать соглашение о вызове функции лямбда С++?

  • Если соглашение о вызове не определено, как правильно переубрать пространство стека после вызова лямбда-функции?

  • Является ли компилятор автоматически генерировать несколько версий лямбда-функции? то есть в виде следующего псевдокода:

    [] __stdcall() {};

    [] __cdecl() {}; и др.

4b9b3361

Ответ 1

В VС++ 2012 компилятор автоматически выбирает преобразование для безстоящих lambdas (у которого нет переменных захвата), когда вы конвертируете "неактивный lambda в указатель функции".

Возможности MSDN С++ 11:

Лямбда

[...] Кроме того, в Visual С++ в Visual Studio 2012 без учета состояния lambdas конвертируются в указатели на функции. [...] (Visual С++ в Visual Studio 2012 еще лучше, потому что мы превратили безгарантийный lambdas конвертируемый в указатели функций, которые имеют условные условные соглашения. Это важно, когда вы используете API, которые ожидают такие вещи, как __stdcall указатели функций.)


Редакция:

NB: Вызывающее преобразование выходит за пределы С++ Standard, оно зависит от другой спецификации, такой как платформа ABI (двоичный интерфейс приложения).

Следующие ответы основаны на выходном ассемблерном коде с параметром /FAs. Так что это просто догадка, и, пожалуйста, попросите Microsoft более подробно: P

Q1. Что называется вызывающим соглашением лямбда-функции С++?

Q3. Если соглашение о вызове не определено, как правильно утилизировать пространство стека после вызова лямбда-функции?

Прежде всего, С++ lambda (-expression) НЕ является функцией (или указателем функции), вы можете вызвать operator() на лямбда-объект, как нормальная функция вызова. И код сборки сборки говорит, что VС++ 2012 генерирует лямбда-тело с преобразованием вызова __thiscall.

Q2. Как указать вызывающее соглашение для функции лямбда С++?

AFAIK, нет способа. (Это может быть только __thiscall)

Q4. Компилятор автоматически генерирует несколько версий лямбда-функции? то есть в виде следующего псевдокода: [...]

Возможно, нет. Лямбда-тип VС++ 2012 обеспечивает только одну реализацию лямбда-тела (void operator()()), но предоставляет множественное "определяемое пользователем преобразование в указатель функции" для каждого преобразования вызова (указатель функции возврата оператора с void (__fastcall*)(void), void (__stdcall*)(void) и void (__cdecl*)(void)).

Вот пример:

// input source code
auto lm = [](){ /*lambda-body*/ };

// reversed C++ code from VC++2012 output assembly code
class lambda_UNIQUE_HASH {
  void __thiscall operator()() {
    /* lambda-body */
  }
  // user-defined conversions
  typedef void (__fastcall * fp_fastcall_t)();
  typedef void (__stdcall * fp_stdcall_t)();
  typedef void (__cdecl * fp_cdecl_t)();
  operator fp_fastcall_t() { ... }
  operator fp_stdcall_t() { ... }
  operator fp_cdecl_t() { ... }
};
lambda_UNIQUE_HASH lm;

Ответ 2

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

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

В качестве примера мы могли бы сделать это:

#include <iostream>

void __cdecl h1() {}
void __stdcall h2(){}

// I'm lazy: 
typedef decltype(&h1) cdecl_nullary_ptr;
typedef decltype(&h2) stdcall_nullary_ptr;

template<typename StatelessNullaryFunctor>
struct make_cdecl {
  static void __cdecl do_it() {
    StatelessNullaryFunctor()();
  }
};
template<typename StatelessNullaryFunctor>
struct make_stdcall {
  static void __stdcall do_it() {
    StatelessNullaryFunctor()();
  }
};

struct test {
  void operator()() const { hidden_implementation(); }

  operator cdecl_nullary_ptr() const {
    return &make_cdecl<test>::do_it;
  }
  operator stdcall_nullary_ptr() const {
    return &make_stdcall<test>::do_it;
  }
};

где наш test бездействующий нулевой класс может быть преобразован в оба указателя функции cdecl и stdcall неявно.

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