Безопасно ли преобразовывать шаблонную лямбду в `void *`?

Я работаю над реализацией волокон с использованием сопрограмм, реализованных на ассемблере. Сопрограммы работают cocall для изменения стека.

Я хотел бы показать это на С++, используя интерфейс более высокого уровня, поскольку сборка cocall может обрабатывать только один аргумент void*.

Чтобы обрабатывать шаблонные лямбда-выражения, я экспериментировал с преобразованием их в void* и обнаружил, что, хотя он компилируется и работает, мне остается только гадать, безопасно ли это делать, предполагая семантику владения стеком (которая сохраняется волокнами ).

template <typename FunctionT>
struct Coentry
{
    static void coentry(void * arg)
    {
        // Is this safe?
        FunctionT * function = reinterpret_cast<FunctionT *>(arg);

        (*function)();
    }

    static void invoke(FunctionT function)
    {
        coentry(reinterpret_cast<void *>(&function));
    }
};

template <typename FunctionT>
void coentry(FunctionT function)
{
    Coentry<FunctionT>::invoke(function);
}


int main(int argc, const char * argv[]) {
    auto f = [&]{
        std::cerr << "Hello World!" << std::endl;
    };

    coentry(f);
}

Это безопасно и, кроме того, эффективно? Преобразовывая в void*, заставляю ли я компилятор выбирать менее эффективное представление?

Кроме того, при вызове coentry(void*) в другом стеке, но исходный invoke(FunctionT) вернулся, есть ли шанс, что стек может оказаться недопустимым для возобновления? (было бы похоже, скажем, на вызов внутри std::thread, я думаю).


person ioquatix    schedule 03.07.2017    source источник
comment
Я не знаю, правильно это или нет, но это заставляет меня кричать внутренне. Почему вы хотите использовать void * для передачи вызываемого объекта? Почему бы не использовать тип шаблона FunctionT? Или std::function?   -  person Some programmer dude    schedule 03.07.2017
comment
Я думал, что объяснил это, но, потому что текущая реализация cocall может обрабатывать только один аргумент void * для нового стека.   -  person ioquatix    schedule 03.07.2017
comment
Вы должны по крайней мере добавить static_assert(sizeof(void *) == sizeof(&function));, потому что указатель на функцию может не иметь того же размера, что и указатель на void.   -  person user7860670    schedule 03.07.2017
comment
C не позволяет приводить указатели функций к void*, и я знаю, что это было аналогично в С++ - если это не изменилось недавно (о чем я не знаю), вы все равно делаете незаконные вещи...   -  person Aconcagua    schedule 03.07.2017
comment
@VTT не должны ли все указатели быть одинакового размера?   -  person ioquatix    schedule 03.07.2017
comment
Нет, размер указателей может варьироваться. В x86 размер указателя на обычную функцию (лямбда в вашем случае — это просто обычная функция, поскольку она ничего не захватывает) такой же, как размер указателя данных. Но это может варьироваться. Размер указателя на функцию-член намного больше. Также в этом примере вы можете передать указатель на лямбда-объект, а не на функцию.   -  person user7860670    schedule 03.07.2017
comment
@Aconcagua Интересно, у меня включены все предупреждения, включая педантичные, и я не получаю никаких сообщений о том, что они недействительны.   -  person ioquatix    schedule 03.07.2017
comment
@Aconcagua Если бы я понял это вопрос правильно, это разрешено начиная с C++11.   -  person Rakete1111    schedule 03.07.2017
comment
@ioquatix Может быть какой-то специальный процессор, в котором указатель функции и указатель данных не имеют одинакового размера (это как-то связано с размером шины адреса, данных и инструкций), но на обычном процессоре сегодня они имеют тот же размер.   -  person mch    schedule 03.07.2017
comment
@Aconcagua - К счастью, лямбда - это не указатель на функцию, а объект.   -  person StoryTeller - Unslander Monica    schedule 03.07.2017
comment
@VTT - здесь лямбда не будет распадаться на указатель функции. FunctionT будет выводиться как тип закрытия лямбды, объекта.   -  person StoryTeller - Unslander Monica    schedule 03.07.2017
comment
@StoryTeller Достаточно интересно: лямбда-выражения с пустым списком захвата могут быть назначены указателям на функции. Что это тогда? Действительно функции или какой-то совместимый вызываемый объект?   -  person Aconcagua    schedule 03.07.2017
comment
@Aconcagua - Функторы. Лямбда-выражения без захвата определяют преобразование в указатель на функцию, но сами по себе являются объектами. И в этом контексте это имеет огромное значение, потому что выведенный тип относится к объекту.   -  person StoryTeller - Unslander Monica    schedule 03.07.2017


Ответы (1)


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

Тем не менее, лямбда является фактическим значением, и если оно сохраняется в автоматическом хранилище, оно сохраняется только до тех пор, пока хранится кадр стека.

Вы можете исправить это несколькими способами. std::function — это одно, другое — хранить лямбду в shared_ptr<void> или unique_ptr<void, void(*)(void*)>. Если вам не нужно стирание типа, вы можете даже сохранить лямбду в структуре с выведенным типом.

Первые два легкие. Третий;

template <typename FunctionT>
struct Coentry {
  FunctionT f;
  static void coentry(void * arg)
  {
     auto* self = reinterpret_cast<Coentry*>(arg);

    (self->f)();
  }
  Coentry(FunctionT fin):f(sts::move(fin)){}
};
template<class FunctionT>
Coentry<FunctionT> make_coentry( FunctionT f ){ return {std::move(f)}; }

теперь держите Coentry рядом достаточно долго, пока задание не завершится.

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

person Yakk - Adam Nevraumont    schedule 03.07.2017
comment
Это выглядит очень интересно, я буду экспериментировать с этим подходом. Я ценю пример кода, так как он действительно помогает понять, что вы имеете в виду. - person ioquatix; 04.07.2017