Есть ли способ принудительно использовать эти экземпляры только в стеке?
У меня есть класс С++, для которого я только хочу, чтобы он был создан в стеке. Я использую api для доступа к контенту, который был разработан на другом (интерпретированном) языке, который поставляется с собственной сборкой мусора. Механизмы на этом языке знают достаточно, чтобы оставить какой-либо контент, на который он ссылается только на стек, и поскольку этот родной класс содержит такую ссылку, жизненно важно, чтобы для правильного поведения пользователь собственного класса С++ никогда не пытайтесь выделить экземпляр этого в другом месте.
Обратите внимание: я не только хочу запретить экземпляр моего класса выделяться новым (если это все, что мне нужно было сделать, я мог бы перегрузить оператор класса new
и сделать его закрытым или явно удалить его с тех пор С++ 11), но также запретить любые статические или возможные глобальные экземпляры класса. Единственный действительный способ безопасного создания этого класса должен быть в стеке, и я хотел бы как-то это гарантировать. Насколько я знаю, создание new
private или удаление его также не препятствует объявлению другого класса с моим классом в качестве переменной-члена и экземпляром, который выделяется в куче.
Как я сейчас управляю этим, это иметь слово "Локальный" как часть имени класса как дружественное напоминание пользователю о том, что экземпляр предназначен только для использования в стеке, но, разумеется, это на самом деле не выполняется компилятором или каким-либо другим механизмом, и я предпочел бы более эффективное решение.
В идеале я хочу обеспечить это во время компиляции и сбоя при неправильном использовании. Если это просто невозможно, бросание исключения во время выполнения, когда экземпляр сконструирован, остается приемлемым отступлением. Решения, которые работают на С++ 11 или С++ 14, прекрасны.
Обратите внимание, что этот вопрос определенно НЕ совпадает с этим, который только хотел предотвратить allocaton с new
Ответы
Ответ 1
Отказ от ответственности: "стек" не является частью стандартного С++, я знаю, что у нас есть ASDV (переменные продолжительности автоматического хранения). ABI может определять стек. Обратите внимание, что иногда они передаются в регистры, которые, я считаю, в порядке.
Определить стиль CPS (стиль продолжения) factory:
class A {
public:
template<typename F, typename... Args>
static auto cps_make(F f, Args&&... args) {
return f(A(std::forward<Args>(args)...));
}
private:
A(/* ... */) {}
A(const A&) = delete;
A(A&&) = delete;
};
Использование: передать лямбда, взяв A и параметры ctor A. Например:
return A::cps_make([&](A a) {
/* do something with a */
return true;
});
Аргументы функции всегда являются ASDV внутри.
Как работает код: cps_make принимает функтор (обычно лямбда), который принимает экземпляр данного типа; и необязательные параметры ctor. Он создает экземпляр (путем перенаправления любых необязательных параметров в ctor), вызывает функтор и возвращает возвращаемый функтор. Поскольку функтор может быть лямбдой в С++ 11, он не нарушает нормальный поток кода.
Красота CPS заключается в том, что вы можете иметь статический полиморфизм, используя авто-лямбда в С++ 14: ваш cps_make() может создавать практически все, что вы пожелаете (иерархия, вариант, любой и т.д.). Затем вы сохраняете виртуальные накладные расходы для закрытых иерархий. У вас может даже быть лямбда для нормального потока, а один, если ctor потерпит неудачу; это удобно, когда исключения не идут.
Недостатком является то, что в настоящее время вы не можете напрямую использовать инструкции управления потоком внешней области внутри лямбда. /* Подсказка: мы работаем над этим. */
Ответ 2
Хорошо, так вот мой прием:
struct stack_marker
{
thread_local static uint8_t* marker;
uint8_t stk;
stack_marker()
{
if (marker != nullptr)
{
throw std::runtime_error("second twice marker! don't do it");
}
marker = &stk;
}
};
thread_local uint8_t* stack_marker::marker = nullptr;
void sort3(uint8_t* (&a)[3]); //sorts 3 pointers, see gist
class only_on_stack
{
uint8_t place;
public:
NO_INLINE only_on_stack(int x)
{
uint8_t a;
if (!stack_marker::marker)
{
// not initialized yet, either forgot to put stack_marker in main
// or we are running before main, which is static storage
//throw std::runtime_error("only on stack object created in non-stack");
std::cout << x << ": I'm NOT on stack\n";
return;
}
uint8_t* ptrs[] = {
stack_marker::marker,
&place,
&a
};
sort3(ptrs);
if (ptrs[1] == &place) // place must be in the middle
{
std::cout << x << ": I'm on stack\n";
}
else
{
//throw std::runtime_error("only_on_stack object created in non-stack");
std::cout << x << ": I'm NOT on stack\n";
}
}
};
only_on_stack static_storage(1);
thread_local only_on_stack tl_storage(4);
int NO_INLINE stuff()
{
only_on_stack oos(2);
}
int main()
{
stack_marker mrk;
stuff();
auto test = new only_on_stack(3);
tl_storage; // access thread local to construct, or gcc omits it
}
По общему признанию, мое решение не является самым чистым из всех, но оно позволяет вам использовать регулярный локальный синтаксис объекта.
В принципе, трюк состоит в том, чтобы поместить в стек 2 дополнительных объекта, кроме нашего объекта: один в начале потока и один в конструкторе. Поэтому один из объектов создается в стеке после нашего объекта и одного из них раньше. С помощью этой информации мы могли бы просто проверить порядок адресов этих трех объектов. Если объект действительно находится в стеке, его адрес должен быть посередине.
Однако С++ не определяет адресный порядок объектов в области функций, поэтому делает что-то вроде этого:
int main()
{
int a;
int b;
int c;
}
Обеспечивает ли не, что &b
находится в середине &a
и &c
.
Чтобы обойти это, мы могли бы сохранить a
в основной функции и переместить b
и c
в другую силу не-вложенную функцию:
void NO_INLINE foo()
{
int b;
int c;
}
int main()
{
int a;
foo();
}
В этом случае, поскольку компилятор не может знать локальные переменные foo
в main
, &a
> &b
, &c
или &a
< &b
, &c
. Применяя одно и то же к c
, переместив его в другую не-встроенную функцию, мы могли бы гарантировать, что & b находится в середине &a
и &c
.
В моей реализации функция stuff
- это функция foo
, а функция, которую мы перемещаем c
в, является конструктором only_on_stack
.
Фактическая рабочая реализация находится здесь: https://gist.github.com/FatihBAKIR/dd125cf4f06cbf13bb4434f79e7f1d43
Он должен работать, растет ли стек вверх или вверх и независимо от типа объектного файла и, надеюсь, ABI, если компилятор не каким-то образом переупорядочивает локальные переменные не-встроенных функций.
Это было протестировано с помощью -O3
на g++ - 6 на linux и последнем clang на mac os x. Он должен работать на MSVC, надеюсь, кто-то сможет его протестировать.
Вывод из обоих:
1: I'm NOT on stack
2: I'm on stack
3: I'm NOT on stack
4: I'm NOT on stack
Использование в основном, вы помещаете объект stack_marker
в начале каждого потока (main
в комплекте) и вызываете другую не встроенную функцию и используете ее как свою фактическую точку входа.
Ответ 3
Возможность состоит в том, чтобы разрешить только временные переменные (с расширенным временем жизни), что-то вроде:
class A
{
private:
A() = default;
A(const A&) = delete;
A(A&&) = delete;
A& operator =(const A&) = delete;
A& operator =(A&&) = delete;
public:
static A Make() { return {}; }
};
auto&& g = A::Make(); // possible :/
int main() {
auto&& a = A::Make(); // possible
#if 0
new A(); // error
struct InnerA
{
A a; // error
};
#endif
}
Он больше не будет действителен в С++ 17 с гарантированным копированием.