Время жизни временного оператора присваивания для проверки и выравнивания alloca()

В процессе тестирования и анализа большой базы кода мне приходится заменять тысячи вызовов alloca() собственным методом. На моей целевой платформе alloca() не работает, если число равно нулю, поэтому мы хотим утверждать, что это так. Мы также хотим предоставить версию выравнивания, которая является одним вызовом. Однако alloca() имеет определенное время жизни, связанное с областью действия, поэтому, хотя я хотел бы написать

void * CheckAndAllocate( size_t sizeInBytes )
{
  assert( sizeInBytes > 0 );
  return alloca(p); // not safe; allocation goes out of scope on return
}

void * p = CheckAndAllocate( sizeInBytes );

Очевидно, что это не вариант, поскольку alloca() не переживет CheckAndAllocate().

Пытаясь решить эту проблему, я написал эту альтернативу, используя временную:

struct CheckSize
{
  inline CheckSize( size_t size ) { assert( size > 0 ); }
  inline void * operator=(void* other) { return other );
}

#define my_alloca(size) CheckSize(size) = alloca(size)

void foo(size_t size)
{
  void * p = my_alloca(size);
  // becomes
  void * p = CheckSize(size) = alloca(size);

  // ... use p locally for work
}

Кроме того, я также определил свою выровненную версию как:

struct CheckSizeAndAlign
{
  size_t _align;
  inline CheckSizeAndAlign( size_t size, size_t align ) : _align(align) { assert( size > 0 ); }
  inline void * operator=(void* other) { return AlignUp(other, _align  ); }

#define my_alloca_aligned( size, align ) CheckSizeAndAlign(size, align ) = alloca(size + align)

void foo(size_t size, size_t alignment)
{
  void * p = my_alloca_aligned( size, alignment);
  // becomes
  void * p = CheckSizeAndAlign( size, alignment) = alloca( size + alignment )

  // ... use p locally for work
}

Мой вопрос: учитывая, что значение, возвращаемое alloca, проходит через временное, нарушает ли это выделение alloca из-за какой-либо области видимости?

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

Мне также просто любопытно узнать об этом конкретном шаблоне.

Это MSVC2019, CLANG и нишевые компиляторы систем реального времени.


person Steven    schedule 24.04.2020    source источник
comment
Какой компилятор/набор инструментов вы используете? alloca не является стандартной функцией, и ее поведение может различаться у разных поставщиков.   -  person 1201ProgramAlarm    schedule 24.04.2020
comment
thousands of calls to alloca() Мне интересно - почему было принято решение использовать alloca() вместо динамического размещения и интеллектуальных указателей?   -  person KamilCuk    schedule 24.04.2020
comment
Перечитайте вопрос. Можете ли вы использовать какие-либо современные функции C++? В частности, лямбды.   -  person Stephen Newell    schedule 24.04.2020
comment
Вы можете просто изменить его - alloca(CheckSize(size)) - и избежать дублирования размера. Затем CheckSize может иметь утверждение (или любую другую проверку, которую вы хотите) и возвращать (возможно, измененный) параметр, чтобы избежать неправильного вызова alloca(0) (хотя некоторые библиотеки с этим согласны).   -  person 1201ProgramAlarm    schedule 24.04.2020
comment
@ 1201ProgramAlarm Это сработает в первом случае, но не для alloca_aligned()   -  person Steven    schedule 24.04.2020
comment
@KamilCuk на самом деле не является частью вопроса, но устаревший код, система реального времени, отсутствие виртуальной памяти и т. д.   -  person Steven    schedule 24.04.2020
comment
@StephenNewell Все, что не выделяет кучу и достаточно производительно   -  person Steven    schedule 24.04.2020


Ответы (1)


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

#include <alloca.h>
#include <cassert>
#include <iostream>
#include <utility>

namespace {
    template <typename FN, typename ...ARGS>
    auto alloc_helper(std::size_t size, FN fn, ARGS && ...args) {
        assert(size > 0);
        auto p = ::alloca(size);
        return fn(p, std::forward<ARGS>(args)...);
    }
}

int main() {
    auto weird_add = [](void * p, auto init, auto other) {
        auto ip = reinterpret_cast<int *>(p);
        *ip = init;
        return *ip + other;
    };

    std::cout << alloc_helper(sizeof(int), weird_add, 10, 17) << '\n';
    return 0;
}

Преимущество этого решения заключается в том, что размер вводится только один раз, плюс указатель, возвращаемый alloca, ограничен именно тем, что вы хотите его использовать; если обратный вызов не returns (вы могли бы assert на этом, но это было бы уродливее). Вы можете создать дополнительные вспомогательные функции для использования других нединамических распределений (например, вы упомянули alloca_aligned в комментарии).

person Stephen Newell    schedule 24.04.2020
comment
Разве тот факт, что ::alloca() происходит внутри alloc_helper() {}, не означает, что выделенная память имеет шанс выйти за пределы области видимости (поскольку стек раскручивается) при возврате? - person Steven; 24.04.2020
comment
Да, это один из пунктов. Выделенная память теперь ограничена более жестко. Он действителен только на время обратного вызова. - person Stephen Newell; 24.04.2020
comment
О, понятно; я бы сделал всю работу с памятью внутри обратного вызова? Думаю, я мог бы это сделать, но тогда для обратного вызова потребовался бы доступ к чему-либо из вызывающей функции, необходимой для выполнения работы. - person Steven; 24.04.2020
comment
@Steven: Lambdas делают это без проблем и без каких-либо выделений. - person Mooing Duck; 24.04.2020
comment
@MooingDuck Да, я понимаю это. К сожалению, это потребует модификации всех областей, в которых мы сейчас используем alloca, что, я думаю, может нарушить сделку. Хотя это интересная идея. - person Steven; 24.04.2020
comment
Если я неправильно понял ваши цели, я удалю или изменю свой ответ. Я думал, вы хотели убедиться, что выделенный размер больше 0 (ваше предложение требовало ввода размера дважды, мое — один раз), и вы были обеспокоены тем, что результат alloca используется после восстановления. - person Stephen Newell; 24.04.2020
comment
@StephenNewell Нет, я извиняюсь, если мои цели неясны - я хочу проверить параметры и, при желании, выровнять распределение, которое будет использоваться в существующей области, не меняя слишком много кода. Некоторые из этих функций довольно сложны, поэтому их переделка кажется невозможной. Таким образом, я не думаю, что ваш ответ применим, хотя это полезный шаблон. - person Steven; 24.04.2020
comment
Итак, более простое решение: почему бы просто не использовать макрос? Что-то вроде #define CheckSize(size) alloca(size); assert(size >0)? - person Stephen Newell; 24.04.2020
comment
@StephenNewell, разве ты не хочешь сделать assert() перед alloca(), а не после? - person Remy Lebeau; 25.04.2020
comment
Честно говоря, я не рассматривал возможность передачи имени переменной. Я пытался придерживаться исходного примера, который привел вопрошавший: void * p = CheckAndAllocate( sizeInBytes ); - person Stephen Newell; 25.04.2020